Compare commits

...

54 Commits

Author SHA1 Message Date
Younes ENNAJI fd65254d63 docs: move Try All Themes section to top of themes page 2026-03-01 22:17:40 +00:00
Younes ENNAJI 33ac9013d5 build assets 2026-03-01 22:16:54 +00:00
Younes ENNAJI 1cc6a7c537 docs: remove hero sections from playground and themes pages 2026-03-01 22:15:18 +00:00
Younes ENNAJI dfe9a12fe1 docs: remove old themes.md (replaced by themes/index.html) 2026-03-01 22:11:08 +00:00
Younes ENNAJI 8074bb1f90 docs: add themes gallery page at /themes/
Features:
- Hero section with animated gradient background
- Themes organized by category (Brand-Inspired, Gemstone, Minimal)
- Visual cards with gradient previews for each theme
- Interactive "Try All Themes" section with demo buttons
- Usage code examples for setting default or per-notification themes
- CTA linking to playground
- Added "All Themes" link to navigation menu
2026-03-01 22:11:00 +00:00
Younes ENNAJI 03942aa634 build assets 2026-03-01 22:04:32 +00:00
Younes ENNAJI 6abae0bdde docs: add interactive playground page
Features:
- Hero section with animated background elements
- Embedded flasher-studio interactive component
- Quick theme preview buttons (12 popular themes)
- Pro tips section for better user experience
- CTA section linking to Laravel/Symfony guides
- Added playground to navigation menu
2026-03-01 22:02:13 +00:00
Younes ENNAJI 05c15399ac docs: restore Palestine support banner 2026-03-01 21:57:14 +00:00
Younes ENNAJI 1bd85021d9 docs: redesign root README for better first impression
Key improvements:
- Strong tagline: "One line of PHP. Beautiful notifications. Zero JavaScript"
- Quick Start at the top (installation in 1 command)
- "Why PHPFlasher?" comparison table showing advantages
- Livewire integration section (previously missing)
- Collapsible themes list for better scannability
- Cleaner visual hierarchy with horizontal rules
- Prominent links to docs, playground, and bug reports
- Simplified contributors section using contrib.rocks
- Clear calls to action for starring and sharing
2026-03-01 21:54:51 +00:00
Younes ENNAJI 359e6de361 docs: expand adapter READMEs with comprehensive examples
Each adapter README now includes:
- Features section highlighting key capabilities
- Installation instructions for Laravel and Symfony
- Multiple Quick Start code examples
- Configuration options table
- Livewire integration with events
- Global configuration examples
2026-03-01 21:50:11 +00:00
Younes ENNAJI e417105f7a refactor(themes): create shared utilities and design tokens
This refactoring improves code quality and reduces duplication:

## New Files
- `themes/shared/icons.ts` - Centralized SVG icons (getIcon, getCloseIcon)
- `themes/shared/accessibility.ts` - A11y helpers (getA11yString, getCloseButtonA11y)
- `themes/shared/constants.ts` - Standard class names and default titles

## Design Tokens Added (index.scss)
- Spacing scale: --fl-spacing-xs through --fl-spacing-2xl
- Typography scale: --fl-font-size-xs through --fl-font-size-xl
- Close button sizes: --fl-close-sm, --fl-close-md, --fl-close-lg
- Border radius: --fl-radius-sm through --fl-radius-full
- Shadow tokens: --fl-shadow-sm through --fl-shadow-lg
- Animation: --fl-duration-*, --fl-easing-*, --fl-slide-*

## Mixins Updated
- Added `close-button-sized($size)` with sm/md/lg support
- Added `close-button-circular($size)` variant
- Added `close-button-text` for text-style buttons
- Updated existing mixins to use design tokens

## All 17 Themes Refactored
Each theme now uses shared utilities instead of duplicating code:
- Icon code: 20+ lines → 1 function call
- Accessibility: 3 lines → 1 function call
- Class names: via CLASS_NAMES constants

Backwards compatible - no breaking changes.
2026-03-01 21:34:50 +00:00
Younes ENNAJI abd70c1d4b Update main JavaScript bundle 2026-03-01 21:13:14 +00:00
Younes ENNAJI 9acddbda52 docs: update CHANGELOG and Livewire docs with event system
Add documentation for the event dispatching system:
- Update CHANGELOG.md with unreleased features
- Add Toastr events section (click, close, show, hidden)
- Add Noty events section (click, close, show, hover)
- Add Notyf events section (click, dismiss)
- Add Theme events section (generic and theme-specific click)
- Include code examples for Livewire event listeners
2026-03-01 21:11:13 +00:00
Younes ENNAJI 7d6e9b46b8 add event dispatching system for Livewire integration
Implement consistent event dispatching across Noty, Notyf, Toastr adapters and
themes, following the existing SweetAlert pattern. This enables Livewire
integration for all notification types.

JavaScript Events:
- Noty: flasher:noty:show, flasher:noty:click, flasher:noty:close, flasher:noty:hover
- Notyf: flasher:notyf:click, flasher:notyf:dismiss
- Toastr: flasher:toastr:show, flasher:toastr:click, flasher:toastr:close, flasher:toastr:hidden
- Themes: flasher:theme:click (generic), flasher:theme:{name}:click (specific)

PHP Livewire Listeners:
- LivewireListener for each adapter (Noty, Notyf, Toastr)
- ThemeLivewireListener for theme click events
- Registered in service providers when Livewire is bound

This allows Livewire users to listen for notification events and react
accordingly (e.g., noty:click, theme:flasher:click).
2026-03-01 21:05:10 +00:00
Younes ENNAJI f1051e1d7f fix: resolve PHPStan errors and add Vitest to lint script
- Remove unnecessary null coalescing in HtmlPresenter (mainScript is never null)
- Add comprehensive ignore rules for test-related PHPStan warnings
- Add Vitest test runner to bin/lint script

All quality checks now pass:
- Rector
- PHP-CS-Fixer
- PHPStan (level max)
- Composer validation
- PHPLint
- PHPUnit (1281 tests)
- Vitest (219 tests)
2026-03-01 20:24:33 +00:00
Younes ENNAJI 87da42fdea Refine code style and tests 2026-03-01 20:16:44 +00:00
Younes ENNAJI 4d9cda22cf build assets 2026-03-01 20:16:00 +00:00
Younes ENNAJI c58f3c7b40 upgrade dependencies 2026-03-01 20:15:46 +00:00
Younes ENNAJI 47eb66e874 fix: update tests for new validation requirements
Update tests that were using invalid values for HopsStamp (0 is now
invalid, must be >= 1) and update expected output format for
mainScript (now uses json_encode which produces double quotes).
2026-03-01 20:13:57 +00:00
Younes ENNAJI 30de24f054 fix: add null checks and error handling to docs JS controllers
clipboard_controller.js:
- Check if clipboard API is available before using
- Handle clipboard write promise rejection
- Show error icon on failure

prev-next_controller.js:
- Guard against missing navigation element
- Guard against missing span element in links

anchor_controller.js:
- Guard against missing ul element
2026-03-01 20:11:07 +00:00
Younes ENNAJI 162ea87330 fix: prevent memory leaks and handle errors in FlasherPlugin
- Remove DOMContentLoaded listener after it fires to prevent memory leak
- Clean up timer interval and event listeners when notification is removed
- Add null check in stringToHTML to throw clear error for invalid templates
- Store cleanup function on notification element for proper disposal

Added tests for memory leak prevention and error handling.
2026-03-01 20:10:06 +00:00
Younes ENNAJI 8cda9d1eb1 fix: rename shadowed variable in Request::getType()
The parameter \$type was being reassigned to the session value, which
shadows the original parameter and causes confusion for static analysis
tools and maintainers.

Renamed the session value variable to \$value for clarity.
2026-03-01 20:08:28 +00:00
Younes ENNAJI 6d314dbc07 fix: add validation to HopsStamp and DelayStamp
HopsStamp:
- Validate hops amount must be >= 1 (positive integer)
- Negative or zero hops don't make logical sense

DelayStamp:
- Validate delay must be >= 0 (non-negative integer)
- Negative delays don't make logical sense

Both now throw InvalidArgumentException for invalid values, making
configuration errors fail fast with clear messages.
2026-03-01 20:07:54 +00:00
Younes ENNAJI ad5c0f56dd fix: validate limit criteria must be positive integer
Previously, negative limits caused array_slice() to return unexpected
results (all except last N elements) and zero returned an empty array
without clear intent.

Now throws InvalidArgumentException for values < 1, making invalid
configurations fail fast with a clear error message.
2026-03-01 20:06:53 +00:00
Younes ENNAJI fd36c2ec0c fix: validate alias attribute in PresenterCompilerPass
Add validation to ensure services tagged with 'flasher.presenter' have
the required 'alias' attribute. Previously, accessing $attributes['alias']
without checking would throw an "Undefined array key" error.

Now throws a clear InvalidArgumentException with a helpful message.
2026-03-01 20:01:12 +00:00
Younes ENNAJI 5202c86107 fix: handle invalid JSON gracefully in FlasherComponent
The json_decode() with JSON_THROW_ON_ERROR throws an exception before
the ?: operator can provide a fallback value. This caused page crashes
when invalid JSON was passed to the Blade component attributes.

Now using a helper method with try-catch to safely decode JSON and
return an empty array on failure, preventing page crashes.
2026-03-01 20:00:16 +00:00
Younes ENNAJI 9e7bb17faa fix: make OctaneListener invokable for Laravel event dispatcher
Laravel's event dispatcher expects listener classes to be invokable
(have __invoke method) when registered as a class name string.

The OctaneListener was using a handle() method which caused the
listener to never be invoked, resulting in notification state leaking
between Octane requests.

Renamed handle() to __invoke() to fix the issue.
2026-03-01 19:59:25 +00:00
Younes ENNAJI 83dc9e49dc fix: handle null public directory gracefully in InstallCommand
The getPublicDir() method can return null, but the code was directly
concatenating it with a string, which could cause unexpected behavior.

Now the command properly checks for null and returns a failure with
a clear error message instead of proceeding with an invalid path.
2026-03-01 19:57:50 +00:00
Younes ENNAJI 1d81de581b fix: escape nonce and mainScript to prevent XSS vulnerabilities
The HtmlPresenter was interpolating user-controlled values directly into
HTML attributes and JavaScript code without proper escaping, creating
XSS vulnerabilities.

Changes:
- Escape nonce with htmlspecialchars() for HTML attribute context
- Escape nonce with json_encode() for JavaScript string context
- Escape mainScript with json_encode() for JavaScript string context

Added tests to verify XSS payloads are properly escaped.
2026-03-01 19:55:49 +00:00
Younes ENNAJI 2ebdbecda6 fix: correct inverted Livewire registration condition
The condition in registerLivewire() was inverted, causing the Livewire
listener to never be registered when Livewire was actually available.

Before: returned early when Livewire class exists AND is NOT bound
After: returns early when Livewire class does NOT exist OR is NOT bound

This fixes Livewire notifications not being displayed in Livewire components.
2026-03-01 19:53:39 +00:00
Younes ENNAJI 851e0a00ed improve Translator test coverage to 100% 2026-02-26 06:13:13 +00:00
Younes ENNAJI d9b0b6998e add tests for FlasherDataCollector 2026-02-26 06:04:11 +00:00
Younes ENNAJI ed992d78f6 add tests for FlasherAssert and Constraint classes 2026-02-25 20:45:32 +00:00
Younes ENNAJI 0612a3bb61 exclude .phpstorm.meta.php from coverage 2026-02-25 20:35:27 +00:00
Younes ENNAJI c9a61ba69c add tests for helper functions 2026-02-25 20:29:51 +00:00
Younes ENNAJI f9746f607b add Laravel Facade tests 2026-02-25 20:22:38 +00:00
Younes ENNAJI 18c2233baa add tests for remaining Criteria classes 2026-02-25 20:01:29 +00:00
Younes ENNAJI 08c242b45a fix test to properly suppress expected console error 2026-02-25 19:50:01 +00:00
Younes ENNAJI cd53ceb139 improve test coverage for edge cases 2026-02-25 19:31:21 +00:00
Younes ENNAJI 549c36eeee improve test coverage for PHPUnit and Vitest 2026-02-25 19:16:53 +00:00
Younes ENNAJI ea0dccc961 add test coverage for PHPUnit and Vitest 2026-02-25 15:58:13 +00:00
Younes ENNAJI d33de77835 add vitest for JS/TS testing with comprehensive test coverage 2026-02-25 15:52:21 +00:00
Younes ENNAJI 62848e0fd1 update composer.lock 2026-02-25 11:37:47 +00:00
Younes ENNAJI c5059adac7 fix PHPStan errors in ThemeTemplatesTest 2026-02-25 11:34:09 +00:00
Younes ENNAJI 4145b870dd fix PHPStan errors in SessionBag 2026-02-25 11:34:01 +00:00
Younes ENNAJI 18a31f578b use PHP 8.2 for lint task 2026-02-25 11:33:40 +00:00
Younes ENNAJI f20bdebda0 add bootstrap and tailwindcss theme templates for Laravel 2026-02-25 11:20:29 +00:00
Younes ENNAJI f9807e91e2 add FallbackSession for stateless requests in Laravel 2026-02-25 11:15:34 +00:00
Younes ENNAJI e35339dca9 fix typo in FilterCriteria error message 2026-02-25 11:00:32 +00:00
Younes ENNAJI f9bef40ae6 fix misleading error messages in HopsCriteria and DelayCriteria 2026-02-25 10:43:52 +00:00
Younes ENNAJI 670e40dc97 fix StampsCriteria comparing values instead of keys 2026-02-25 10:43:44 +00:00
Younes ENNAJI 2b0e736d28 fix multi-field sorting in OrderByCriteria 2026-02-25 10:43:36 +00:00
Younes ENNAJI b79902779e add support for Laravel v13 2026-02-25 10:26:04 +00:00
Younes ENNAJI 8779de6c62 upgrade dependencies 2026-02-25 10:18:29 +00:00
272 changed files with 21658 additions and 2452 deletions
+22
View File
@@ -5,7 +5,28 @@ on:
types: [ published ]
jobs:
test:
runs-on: ubuntu-latest
name: Run Tests Before Publishing
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
- name: 🔧 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: 📦 Install Dependencies
run: npm ci
- name: ✅ Run Tests
run: npm run test
publish-prime:
needs: test
runs-on: ubuntu-latest
defaults:
run:
@@ -30,6 +51,7 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-plugin:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
+41 -1
View File
@@ -10,6 +10,29 @@ on:
- cron: '0 0 * * *' # Daily at midnight
jobs:
javascript-tests:
runs-on: ubuntu-latest
name: JavaScript Tests
steps:
- name: 📥 Checkout Code
uses: actions/checkout@v4
- name: 🔧 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: 📦 Install Dependencies
run: npm ci
- name: ✅ Run Tests
run: npm run test
- name: 📊 Run Coverage
run: npm run test:coverage
static-analysis:
runs-on: ubuntu-latest
strategy:
@@ -156,6 +179,9 @@ jobs:
fail-fast: false
matrix:
include:
- { laravel: 13.x-dev, testbench: 11.x-dev, php: 8.5, phpunit: 11.5.* }
- { laravel: 13.x-dev, testbench: 11.x-dev, php: 8.4, phpunit: 11.5.* }
- { laravel: 13.x-dev, testbench: 11.x-dev, php: 8.3, phpunit: 11.5.* }
- { laravel: 12.*, testbench: 10.*, php: 8.5, phpunit: 11.5.* }
- { laravel: 12.*, testbench: 10.*, php: 8.4, phpunit: 11.5.* }
- { laravel: 12.*, testbench: 10.*, php: 8.3, phpunit: 11.5.* }
@@ -186,8 +212,22 @@ jobs:
run: |
sed -i '/\"require\": {/,/},/d; /\"require-dev\": {/,/},/d' composer.json
composer config --global allow-plugins true
if [[ "${{ matrix.laravel }}" == *"-dev"* ]]; then
echo "Setting minimum-stability to dev for ${{ matrix.laravel }}"
composer config minimum-stability "dev"
composer config prefer-stable true
fi
composer require "laravel/framework:${{ matrix.laravel }}" "phpunit/phpunit:${{ matrix.phpunit }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --prefer-lowest -W --no-interaction --prefer-dist --optimize-autoloader
if [[ "${{ matrix.laravel }}" == *"-dev"* ]]; then
echo "Running composer update (latest dependencies)"
composer update -W --no-interaction --prefer-dist --optimize-autoloader
else
echo "Running composer update --prefer-lowest"
composer update --prefer-lowest -W --no-interaction --prefer-dist --optimize-autoloader
fi
- name: ✅ Execute tests
run: vendor/bin/phpunit --testsuite laravel
+2
View File
@@ -4,6 +4,8 @@
/vendor/
/node_modules/
/coverage/
/.cache/php-cs-fixer/
/.cache/phplint/
/.cache/phpstan/
+7
View File
@@ -2,6 +2,13 @@
## [Unreleased](https://github.com/php-flasher/php-flasher/compare/v2.1.4...2.x)
* feature [Flasher] Add event dispatching system for all notification adapters and themes with Livewire integration:
- [Toastr] Dispatch events: `flasher:toastr:click`, `flasher:toastr:close`, `flasher:toastr:show`, `flasher:toastr:hidden`
- [Noty] Dispatch events: `flasher:noty:click`, `flasher:noty:close`, `flasher:noty:show`, `flasher:noty:hover`
- [Notyf] Dispatch events: `flasher:notyf:click`, `flasher:notyf:dismiss`
- [Themes] Dispatch events: `flasher:theme:click` (generic) and `flasher:theme:{name}:click` (specific)
- [Laravel] Add LivewireListener classes for all adapters and themes to enable Livewire event handling
## [v2.1.3](https://github.com/php-flasher/php-flasher/compare/v2.1.2...v2.1.3) - 2025-01-25
* bug [#208](https://github.com/php-flasher/php-flasher/issues/208) [Flasher] Add GitHub workflow for automatic publishing of assets to NPM. See [PR #211](https://github.com/php-flasher/php-flasher/pull/211) by [ToshY](https://github.com/ToshY)
+241 -488
View File
@@ -11,316 +11,239 @@
</picture>
</p>
<h1 align="center">Elegant Flash Notifications for PHP</h1>
<p align="center">
<a href="https://www.linkedin.com/in/younes--ennaji"><img src="https://img.shields.io/badge/author-@yoeunes-blue.svg" alt="Author Badge"></a>
<a href="https://github.com/php-flasher/php-flasher"><img src="https://img.shields.io/badge/source-php--flasher/php--flasher-blue.svg" alt="Source Code Badge"></a>
<a href="https://github.com/php-flasher/php-flasher/releases"><img src="https://img.shields.io/github/tag/php-flasher/flasher.svg" alt="GitHub Release Badge"></a>
<a href="https://github.com/php-flasher/flasher/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-brightgreen.svg" alt="License Badge"></a>
<a href="https://packagist.org/packages/php-flasher/flasher"><img src="https://img.shields.io/packagist/dt/php-flasher/flasher.svg" alt="Packagist Downloads Badge"></a>
<a href="https://github.com/php-flasher/php-flasher"><img src="https://img.shields.io/github/stars/php-flasher/php-flasher.svg" alt="GitHub Stars Badge"></a>
<a href="https://packagist.org/packages/php-flasher/flasher"><img src="https://img.shields.io/packagist/php-v/php-flasher/flasher.svg" alt="Supported PHP Version Badge"></a>
<strong>One line of PHP. Beautiful notifications. Zero JavaScript.</strong>
</p>
# PHPFlasher: Beautiful Notifications Made Simple
<p align="center">
<a href="https://packagist.org/packages/php-flasher/flasher"><img src="https://img.shields.io/packagist/dt/php-flasher/flasher.svg?style=flat-square&label=downloads" alt="Downloads"></a>
<a href="https://github.com/php-flasher/php-flasher"><img src="https://img.shields.io/github/stars/php-flasher/php-flasher.svg?style=flat-square&label=stars" alt="Stars"></a>
<a href="https://github.com/php-flasher/php-flasher/releases"><img src="https://img.shields.io/github/v/release/php-flasher/flasher.svg?style=flat-square" alt="Release"></a>
<a href="https://packagist.org/packages/php-flasher/flasher"><img src="https://img.shields.io/packagist/php-v/php-flasher/flasher.svg?style=flat-square" alt="PHP Version"></a>
<a href="https://github.com/php-flasher/flasher/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square" alt="License"></a>
</p>
## 🚀 See It In Action
<p align="center">
<a href="https://php-flasher.io"><strong>Documentation</strong></a> ·
<a href="https://php-flasher.io/playground"><strong>Live Playground</strong></a> ·
<a href="https://github.com/php-flasher/php-flasher/issues"><strong>Report Bug</strong></a>
</p>
---
## Quick Start
**Laravel:**
```bash
composer require php-flasher/flasher-laravel && php artisan flasher:install
```
**Symfony:**
```bash
composer require php-flasher/flasher-symfony && php bin/console flasher:install
```
**That's it!** Now use it:
```php
// In your controller
public function updateProfile(Request $request)
{
// Process form submission
$user = Auth::user();
$user->update($request->validated());
// Show a beautiful notification to the user
flash()->success('Your profile has been updated successfully!');
return redirect()->back();
}
flash()->success('Welcome aboard! Your account is ready.');
```
That's it! PHPFlasher will display an elegant success notification to your user. No JavaScript to write, no frontend setup needed.
---
## 🔄 Compatibility
## Why PHPFlasher?
| Requirements | Version |
|--------------|---------|
| PHP | ≥ 8.2 |
| Laravel | ≥ 11.0 |
| Symfony | ≥ 7.0 |
| | PHPFlasher | Others |
|---|:---:|:---:|
| **Zero JavaScript** | Write PHP only, frontend handled automatically | Requires manual JS setup |
| **Auto Asset Injection** | CSS/JS injected automatically | Manual script tags needed |
| **26 Built-in Themes** | Amazon, iOS, Slack, Material & more | Limited or no themes |
| **4 Notification Libraries** | Toastr, SweetAlert, Noty, Notyf | Single library only |
| **Livewire Integration** | Full event system support | Limited or none |
| **RTL Support** | Built-in right-to-left | Often missing |
| **Framework Agnostic** | Laravel, Symfony, or vanilla PHP | Framework-specific |
## 📑 Table of Contents
---
- [Introduction](#-introduction)
- [Features](#-key-features)
- [Installation](#-installation)
- [Usage Examples](#-usage-examples)
- [Themes](#-themes)
- [Adapters](#-adapters)
- [Advanced Configuration](#-advanced-configuration)
- [Adapter Documentation Example](#-adapter-documentation-example)
- [Community & Support](#-community--support)
- [Contributors](#-contributors-and-sponsors)
- [License](#-license)
## 🌟 Introduction
PHPFlasher is a powerful, framework-agnostic library that makes it simple to add beautiful flash messages to your web applications. It provides a consistent API for displaying notifications across your PHP applications, whether you're using Laravel, Symfony, or any other PHP framework.
Flash messages are short-lived notifications that appear after a user performs an action, such as submitting a form. PHPFlasher helps you create these notifications with minimal effort while ensuring they look great and behave consistently.
## ✨ Key Features
- **Zero JavaScript Required**: Write only PHP code - frontend functionality is handled automatically
- **Framework Support**: First-class Laravel and Symfony integration
- **Beautiful Themes**: Multiple built-in themes ready to use out of the box
- **Multiple Notification Types**: Success, error, warning, and info notifications
- **Highly Customizable**: Positions, timeouts, animations, and more
- **Third-Party Adapters**: Integration with popular libraries like Toastr, SweetAlert, and more
- **API Response Support**: Works with AJAX and API responses
- **TypeScript Support**: Full TypeScript definitions for frontend customization
- **Lightweight**: Minimal performance impact
## 📦 Installation
### For Laravel Projects
```bash
# Install the Laravel adapter
composer require php-flasher/flasher-laravel
# Set up assets automatically
php artisan flasher:install
```
PHPFlasher automatically injects the necessary JavaScript and CSS assets into your Blade templates. No additional setup required!
### For Symfony Projects
```bash
# Install the Symfony adapter
composer require php-flasher/flasher-symfony
# Set up assets automatically
php bin/console flasher:install
```
PHPFlasher automatically injects the necessary JavaScript and CSS assets into your Twig templates.
## 📚 Usage Examples
### Basic Notifications
## Notification Types
```php
// Success notification
flash()->success('Operation completed successfully!');
// Error notification
flash()->error('An error occurred. Please try again.');
// Info notification
flash()->info('Your account will expire in 10 days.');
// Warning notification
flash()->error('Oops! Something went wrong.');
flash()->warning('Please backup your data before continuing.');
flash()->info('A new version is available for download.');
```
### With Custom Titles
### With Titles
```php
flash()->success('Your changes have been saved!', 'Update Successful');
flash()->error('Unable to connect to the server.', 'Connection Error');
flash()->success('Your changes have been saved.', 'Update Complete');
flash()->error('Unable to connect to server.', 'Connection Failed');
```
### With Custom Options
### With Options
```php
flash()->success('Profile updated successfully!', [
'timeout' => 10000, // Display for 10 seconds
flash()->success('Profile updated!', [
'position' => 'bottom-right',
'closeButton' => true,
]);
flash()->error('Failed to submit form.', 'Error', [
'timeout' => 0, // No timeout (stay until dismissed)
'position' => 'center',
'timeout' => 10000,
]);
```
### In Controllers
---
## 26 Beautiful Themes
PHPFlasher includes **26 professionally designed themes** ready to use:
```php
// Laravel
public function store(Request $request)
flash()->success('Welcome!', ['theme' => 'amazon']);
flash()->success('Welcome!', ['theme' => 'ios']);
flash()->success('Welcome!', ['theme' => 'slack']);
flash()->success('Welcome!', ['theme' => 'material']);
```
<details>
<summary><strong>View All Themes</strong></summary>
| Theme | Style |
|-------|-------|
| `flasher` | Default clean design |
| `amazon` | Amazon-inspired |
| `bootstrap` | Bootstrap style |
| `ios` | Apple iOS notifications |
| `slack` | Slack messaging style |
| `material` | Google Material Design |
| `tailwind` | Tailwind CSS inspired |
| `google` | Google notifications |
| `facebook` | Facebook style |
| `minimal` | Ultra-clean minimal |
| `amber` | Warm amber tones |
| `aurora` | Gradient effects |
| `crystal` | Transparent design |
| `emerald` | Modern green palette |
| `jade` | Soft jade colors |
| `neon` | Bright attention-grabbing |
| `onyx` | Dark mode sleek |
| `ruby` | Bold ruby accents |
| `sapphire` | Elegant blue style |
| `shadow` | Soft shadow effects |
| `spectrum` | Colorful spectrum |
| `sunset` | Warm sunset colors |
| `zen` | Calm minimal design |
[**See all themes with live demos →**](https://php-flasher.io/themes)
</details>
---
## Notification Libraries
Need more features? Use popular notification libraries:
### Toastr
```bash
composer require php-flasher/flasher-toastr-laravel
```
```php
toastr()->success('Profile saved!', [
'positionClass' => 'toast-bottom-right',
'progressBar' => true,
]);
```
### SweetAlert
```bash
composer require php-flasher/flasher-sweetalert-laravel
```
```php
sweetalert()
->showDenyButton()
->showCancelButton()
->warning('Do you want to save changes?');
```
### Noty
```bash
composer require php-flasher/flasher-noty-laravel
```
```php
noty()->success('Data synchronized!', [
'layout' => 'topCenter',
'timeout' => 3000,
]);
```
### Notyf
```bash
composer require php-flasher/flasher-notyf-laravel
```
```php
notyf()->success('Upload complete!', [
'dismissible' => true,
'ripple' => true,
]);
```
---
## Livewire Integration
PHPFlasher integrates seamlessly with Laravel Livewire:
```php
use Livewire\Attributes\On;
class UserProfile extends Component
{
// Process form...
flash()->success('Product created successfully!');
return redirect()->route('products.index');
}
public function save()
{
// Save logic...
// Symfony
public function store(Request $request, FlasherInterface $flasher)
{
// Process form...
$flasher->success('Product created successfully!');
return $this->redirectToRoute('products.index');
sweetalert()
->showDenyButton()
->success('Save changes?');
}
#[On('sweetalert:confirmed')]
public function onConfirmed(array $payload): void
{
// User clicked confirm
$this->user->save();
flash()->success('Profile saved!');
}
#[On('sweetalert:denied')]
public function onDenied(array $payload): void
{
// User clicked deny
}
}
```
### Specific Use Cases
[**Livewire documentation →**](https://php-flasher.io/livewire)
---
## Configuration
### Laravel
```php
// Form validation errors
if ($validator->fails()) {
flash()->error('Please fix the errors in your form.');
return redirect()->back()->withErrors($validator);
}
// Multi-step process
flash()->success('Step 1 completed!');
flash()->info('Please complete step 2.');
// With HTML content (must enable HTML option)
flash()->success('Your <strong>account</strong> is ready!', [
'escapeHtml' => false
]);
```
## 🎨 Themes
PHPFlasher comes with beautiful built-in themes ready to use immediately:
### Default Theme (Flasher)
The default theme provides clean, elegant notifications that work well in any application:
```php
flash()->success('Operation completed!');
```
### Other Built-In Themes
PHPFlasher includes multiple themes to match your application's design:
```php
// Set the theme in your configuration or per notification
flash()->success('Operation completed!', [
'theme' => 'amazon' // Amazon-inspired theme
]);
// Other available themes:
// - amber: Warm amber styling
// - aurora: Subtle gradient effects
// - crystal: Clean, transparent design
// - emerald: Green-focused modern look
// - facebook: Facebook notification style
// - google: Google Material Design inspired
// - ios: iOS notification style
// - jade: Soft jade color palette
// - material: Material Design implementation
// - minimal: Extremely clean and simple
// - neon: Bright, attention-grabbing
// - onyx: Dark mode sleek design
// - ruby: Bold ruby red accents
// - sapphire: Blue-focused elegant style
// - slack: Slack-inspired notifications
```
### Theme Configuration
You can configure theme defaults in your configuration file:
```php
// Laravel: config/flasher.php
// config/flasher.php
return [
'themes' => [
'flasher' => [
'options' => [
'timeout' => 5000,
'position' => 'top-right',
],
],
'amazon' => [
'options' => [
'timeout' => 3000,
'position' => 'bottom-right',
],
],
],
];
```
## 🧩 Adapters
Beyond PHPFlasher's built-in themes, you can use third-party notification libraries:
### Available Adapters
#### Toastr
```bash
composer require php-flasher/flasher-toastr-laravel # For Laravel
composer require php-flasher/flasher-toastr-symfony # For Symfony
```
```php
flash('toastr')->success('Message with Toastr!');
```
#### SweetAlert
```bash
composer require php-flasher/flasher-sweetalert-laravel # For Laravel
composer require php-flasher/flasher-sweetalert-symfony # For Symfony
```
```php
flash('sweetalert')->success('Message with SweetAlert!');
```
#### Noty
```bash
composer require php-flasher/flasher-noty-laravel # For Laravel
composer require php-flasher/flasher-noty-symfony # For Symfony
```
```php
flash('noty')->success('Message with Noty!');
```
#### Notyf
```bash
composer require php-flasher/flasher-notyf-laravel # For Laravel
composer require php-flasher/flasher-notyf-symfony # For Symfony
```
```php
flash('notyf')->success('Message with Notyf!');
```
## ⚙️ Advanced Configuration
### Laravel Configuration
Publish and customize the configuration:
```bash
php artisan vendor:publish --tag=flasher-config
```
This creates `config/flasher.php` where you can configure:
```php
return [
// Default adapter to use
'default' => 'flasher',
// Theme configuration
'themes' => [
'flasher' => [
'options' => [
@@ -329,257 +252,87 @@ return [
],
],
],
// Auto-convert Laravel session flash messages
'auto_create_from_session' => true,
// Automatically render notifications
'auto_render' => true,
// Type mappings for Laravel notifications
'types_mapping' => [
'success' => 'success',
'error' => 'error',
'warning' => 'warning',
'info' => 'info',
],
];
```
### Symfony Configuration
Create or edit `config/packages/flasher.yaml`:
### Symfony
```yaml
# config/packages/flasher.yaml
flasher:
default: flasher
themes:
flasher:
options:
timeout: 5000
position: top-right
auto_create_from_session: true
auto_render: true
types_mapping:
success: success
error: error
warning: warning
info: info
```
### Notification Options
### Common Options
Common options you can customize:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `timeout` | int | `5000` | Auto-dismiss delay in ms (0 = sticky) |
| `position` | string | `top-right` | `top-right`, `top-left`, `bottom-right`, `bottom-left`, `top-center`, `bottom-center` |
| `closeButton` | bool | `true` | Show close button |
| `progressBar` | bool | `true` | Show timeout progress bar |
| `rtl` | bool | `false` | Right-to-left text direction |
| `escapeHtml` | bool | `true` | Escape HTML in messages |
```php
flash()->success('Message', [
// Display duration in milliseconds (0 = until dismissed)
'timeout' => 5000,
// Notification position
'position' => 'top-right', // top-right, top-left, bottom-right, bottom-left, top-center, bottom-center
// Display a close button
'closeButton' => true,
// Show progress bar during timeout
'progressBar' => true,
// Right-to-left text direction
'rtl' => false,
// Allow HTML content in notifications
'escapeHtml' => false,
// Custom CSS class
'class' => 'my-custom-notification',
]);
```
---
## 📘 Adapter Documentation Example
## Requirements
Here's a detailed example of using the Toastr adapter with PHPFlasher:
| Requirement | Version |
|-------------|---------|
| PHP | >= 8.2 |
| Laravel | >= 11.0 |
| Symfony | >= 7.0 |
### Installation
---
```bash
composer require php-flasher/flasher-toastr
```
## Documentation
For Laravel:
```bash
composer require php-flasher/flasher-toastr-laravel
```
For complete documentation, visit **[php-flasher.io](https://php-flasher.io)**
For Symfony:
```bash
composer require php-flasher/flasher-toastr-symfony
```
- [Installation Guide](https://php-flasher.io/installation)
- [Usage & Examples](https://php-flasher.io/usage)
- [Themes Gallery](https://php-flasher.io/themes)
- [Livewire Integration](https://php-flasher.io/livewire)
- [Configuration Reference](https://php-flasher.io/configuration)
- [API Reference](https://php-flasher.io/api)
### Usage
---
#### Basic Usage
## Contributing
```php
use Flasher\Toastr\Prime\ToastrFactory;
Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
// Inject the factory
public function __construct(private ToastrFactory $toastr)
{
}
## Contributors
public function index()
{
$this->toastr->success('Toastr is working!');
// Or using the global helper with the specified adapter
flash('toastr')->success('Toastr is awesome!');
}
```
<a href="https://github.com/php-flasher/php-flasher/graphs/contributors">
<img src="https://contrib.rocks/image?repo=php-flasher/php-flasher" alt="Contributors" />
</a>
#### With Options
---
```php
flash('toastr')->success('Success message', 'Success Title', [
'timeOut' => 5000,
'closeButton' => true,
'newestOnTop' => true,
'progressBar' => true,
'positionClass' => 'toast-top-right',
]);
```
## Support the Project
#### Available Methods
If PHPFlasher helps you build better applications, please consider:
```php
// Standard notification types
flash('toastr')->success('Success message');
flash('toastr')->info('Information message');
flash('toastr')->warning('Warning message');
flash('toastr')->error('Error message');
- **[Star this repository](https://github.com/php-flasher/php-flasher)** to show your support
- **[Report bugs](https://github.com/php-flasher/php-flasher/issues)** to help improve the library
- **[Share on Twitter](https://twitter.com/intent/tweet?text=Check%20out%20PHPFlasher%20-%20beautiful%20flash%20notifications%20for%20PHP!&url=https://github.com/php-flasher/php-flasher)** to spread the word
// Custom notification
flash('toastr')->flash('custom-type', 'Custom message');
```
---
#### Toastr Specific Options
## License
| Option | Type | Default | Description |
|------------------|---------|----------------|--------------------------------------------|
| closeButton | Boolean | false | Display a close button |
| closeClass | String | 'toast-close-button' | CSS class for close button |
| newestOnTop | Boolean | true | Add notifications to the top of the stack |
| progressBar | Boolean | true | Display progress bar |
| positionClass | String | 'toast-top-right' | Position of the notification |
| preventDuplicates | Boolean | false | Prevent duplicates |
| showDuration | Number | 300 | Show animation duration in ms |
| hideDuration | Number | 1000 | Hide animation duration in ms |
| timeOut | Number | 5000 | Auto-close duration (0 = disable) |
| extendedTimeOut | Number | 1000 | Duration after hover |
| showEasing | String | 'swing' | Show animation easing |
| hideEasing | String | 'linear' | Hide animation easing |
| showMethod | String | 'fadeIn' | Show animation method |
| hideMethod | String | 'fadeOut' | Hide animation method |
### Advanced Configuration
#### Laravel Configuration
Publish the configuration file:
```bash
php artisan vendor:publish --tag=flasher-toastr-config
```
#### Symfony Configuration
Edit your `config/packages/flasher.yaml`:
```yaml
flasher:
toastr:
options:
timeOut: 5000
progressBar: true
```
## Learn More
For additional information, see the [PHPFlasher documentation](https://php-flasher.io).
## 👥 Community & Support
### Getting Help
- **Documentation**: Visit [https://php-flasher.io](https://php-flasher.io)
- **GitHub Issues**: [Report bugs or request features](https://github.com/php-flasher/php-flasher/issues)
- **Stack Overflow**: Ask questions with the `php-flasher` tag
### Common Use Cases
- Form submission feedback
- AJAX request notifications
- Authentication messages
- Error reporting
- Success confirmations
- System alerts
## 🌟 Contributors and Sponsors
Join our team of contributors and make a lasting impact on our project!
We are always looking for passionate individuals who want to contribute their skills and ideas.
Whether you're a developer, designer, or simply have a great idea, we welcome your participation and collaboration.
Shining stars of our community:
<!-- ALL-CONTRIBUTORS-LIST:START -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/younes--ennaji/"><img src="https://avatars.githubusercontent.com/u/10859693?v=4?s=100" width="100px;" alt="Younes ENNAJI"/><br /><sub><b>Younes ENNAJI</b></sub></a><br /><a href="https://github.com/php-flasher/php-flasher/commits?author=yoeunes" title="Code">💻</a> <a href="https://github.com/php-flasher/php-flasher/commits?author=yoeunes" title="Documentation">📖</a> <a href="#maintenance-yoeunes" title="Maintenance">🚧</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/salmayno"><img src="https://avatars.githubusercontent.com/u/27933199?v=4?s=100" width="100px;" alt="Salma Mourad"/><br /><sub><b>Salma Mourad</b></sub></a><br /><a href="#financial-salmayno" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.youtube.com/rstacode"><img src="https://avatars.githubusercontent.com/u/35005761?v=4?s=100" width="100px;" alt="Nashwan Abdullah"/><br /><sub><b>Nashwan Abdullah</b></sub></a><br /><a href="#financial-codenashwan" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://darvis.nl/"><img src="https://avatars.githubusercontent.com/u/7394837?v=4?s=100" width="100px;" alt="Arvid de Jong"/><br /><sub><b>Arvid de Jong</b></sub></a><br /><a href="#financial-darviscommerce" title="Financial">💵</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ashallendesign.co.uk/"><img src="https://avatars.githubusercontent.com/u/39652331?v=4?s=100" width="100px;" alt="Ash Allen"/><br /><sub><b>Ash Allen</b></sub></a><br /><a href="#design-ash-jc-allen" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://about.me/murrant"><img src="https://avatars.githubusercontent.com/u/39462?v=4?s=100" width="100px;" alt="Tony Murray"/><br /><sub><b>Tony Murray</b></sub></a><br /><a href="https://github.com/php-flasher/php-flasher/commits?author=murrant" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/n3wborn"><img src="https://avatars.githubusercontent.com/u/10246722?v=4?s=100" width="100px;" alt="Stéphane P"/><br /><sub><b>Stéphane P</b></sub></a><br /><a href="https://github.com/php-flasher/php-flasher/commits?author=n3wborn" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.instagram.com/lucas.maciel_z"><img src="https://avatars.githubusercontent.com/u/80225404?v=4?s=100" width="100px;" alt="Lucas Maciel"/><br /><sub><b>Lucas Maciel</b></sub></a><br /><a href="#design-LucasStorm" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AhmedGamal"><img src="https://avatars.githubusercontent.com/u/11786167?v=4?s=100" width="100px;" alt="Ahmed Gamal"/><br /><sub><b>Ahmed Gamal</b></sub></a><br /><a href="https://github.com/php-flasher/php-flasher/commits?author=AhmedGamal" title="Code">💻</a> <a href="https://github.com/php-flasher/php-flasher/commits?author=AhmedGamal" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BrookeDot"><img src="https://avatars.githubusercontent.com/u/150348?v=4?s=100" width="100px;" alt="Brooke."/><br /><sub><b>Brooke.</b></sub></a><br /><a href="https://github.com/php-flasher/php-flasher/commits?author=BrookeDot" title="Documentation">📖</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## 📬 Contact
PHPFlasher is being actively developed by <a href="https://github.com/yoeunes">yoeunes</a>.
You can reach out with questions, bug reports, or feature requests on any of the following:
- [Github Issues](https://github.com/php-flasher/php-flasher/issues)
- [Github](https://github.com/yoeunes)
- [Twitter](https://twitter.com/yoeunes)
- [Linkedin](https://www.linkedin.com/in/younes--ennaji/)
- [Email me directly](mailto:younes.ennaji.pro@gmail.com)
## 📝 License
PHPFlasher is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
<p align="center"> <b>Made with ❤️ by <a href="https://www.linkedin.com/in/younes--ennaji/">Younes ENNAJI</a> </b> </p>
PHPFlasher is open-source software licensed under the [MIT license](LICENSE).
<p align="center">
<a href="https://github.com/php-flasher/php-flasher/stargazers">
⭐ Star if you found this useful ⭐
</a>
<br>
<strong>Made with ❤️ by <a href="https://github.com/yoeunes">Younes ENNAJI</a></strong>
<br><br>
<a href="https://github.com/php-flasher/php-flasher/stargazers">⭐ Star if you find this useful!</a>
</p>
Executable
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
set -o pipefail
# PHP binary to use (defaults to PHP 8.2)
PHP_BINARY="${PHP_BINARY:-/opt/homebrew/opt/php@8.2/bin/php}"
# Colors and styles
readonly RESET='\033[0m'
readonly BOLD='\033[1m'
readonly DIM='\033[2m'
readonly BLUE='\033[34m'
readonly GREEN='\033[32m'
readonly RED='\033[31m'
readonly YELLOW='\033[33m'
readonly CYAN='\033[36m'
readonly MAGENTA='\033[35m'
# Essential emojis
readonly ROCKET="🚀"
readonly CHECK="✓"
readonly ERROR="❌"
readonly WARNING="⚠️"
readonly CHART="📊"
readonly TEST_TUBE="🧪"
readonly SPARKLES="✨"
readonly GLOBE="🌐"
print_header() {
echo -e "\n${BOLD}${BLUE}${ROCKET} PHP-Flasher Test Coverage ${ROCKET}${RESET}\n"
echo -e "${BOLD}Date : ${RESET}${CYAN}$(date -u '+%Y-%m-%d %H:%M:%S') UTC${RESET}"
echo -e "${BOLD}Directory : ${RESET}${BLUE}$(pwd)${RESET}\n"
}
cleanup_coverage() {
echo -e "${BOLD}${DIM}Cleaning up old coverage reports...${RESET}"
rm -rf coverage/phpunit coverage/vitest
mkdir -p coverage/phpunit coverage/vitest
echo -e "${CHECK} ${GREEN}Cleanup complete${RESET}\n"
}
run_phpunit_coverage() {
echo -e "${BOLD}${TEST_TUBE} Running PHPUnit with Coverage${RESET}"
if $PHP_BINARY vendor/bin/phpunit --coverage-html coverage/phpunit/html --coverage-text=coverage/phpunit/report.txt; then
echo -e "${CHECK} ${GREEN}PHPUnit coverage generated${RESET}"
echo -e " ${DIM}HTML Report: coverage/phpunit/html/index.html${RESET}\n"
return 0
else
echo -e "${WARNING} ${YELLOW}PHPUnit coverage generation failed${RESET}\n"
return 1
fi
}
run_vitest_coverage() {
echo -e "${BOLD}${TEST_TUBE} Running Vitest with Coverage${RESET}"
if npm run test:coverage; then
echo -e "${CHECK} ${GREEN}Vitest coverage generated${RESET}"
echo -e " ${DIM}HTML Report: coverage/vitest/index.html${RESET}\n"
return 0
else
echo -e "${WARNING} ${YELLOW}Vitest coverage generation failed${RESET}\n"
return 1
fi
}
print_summary() {
echo -e "${BOLD}${CYAN}${CHART} Coverage Reports Summary${RESET}\n"
echo -e "${BOLD}PHPUnit Coverage:${RESET}"
if [ -f "coverage/phpunit/html/index.html" ]; then
echo -e " ${GREEN}${CHECK} HTML: ${RESET}coverage/phpunit/html/index.html"
if [ -f "coverage/phpunit/report.txt" ]; then
echo -e " ${GREEN}${CHECK} Text: ${RESET}coverage/phpunit/report.txt"
fi
else
echo -e " ${RED}${ERROR} No coverage report generated${RESET}"
fi
echo -e "\n${BOLD}Vitest Coverage:${RESET}"
if [ -f "coverage/vitest/index.html" ]; then
echo -e " ${GREEN}${CHECK} HTML: ${RESET}coverage/vitest/index.html"
if [ -f "coverage/vitest/coverage-final.json" ]; then
echo -e " ${GREEN}${CHECK} JSON: ${RESET}coverage/vitest/coverage-final.json"
fi
else
echo -e " ${RED}${ERROR} No coverage report generated${RESET}"
fi
echo -e "\n${BOLD}${GLOBE} Open Reports:${RESET}"
echo -e " ${CYAN}npm run coverage:open${RESET}"
echo -e " ${DIM}Or manually open the HTML files in your browser${RESET}\n"
}
open_reports() {
if [ "$1" == "--open" ] || [ "$1" == "-o" ]; then
echo -e "${BOLD}${GLOBE} Opening coverage reports...${RESET}\n"
if [ -f "coverage/phpunit/html/index.html" ]; then
open coverage/phpunit/html/index.html 2>/dev/null || xdg-open coverage/phpunit/html/index.html 2>/dev/null
fi
if [ -f "coverage/vitest/index.html" ]; then
open coverage/vitest/index.html 2>/dev/null || xdg-open coverage/vitest/index.html 2>/dev/null
fi
fi
}
main() {
local start_time=$(date +%s)
print_header
cleanup_coverage
run_phpunit_coverage
run_vitest_coverage
local end_time=$(date +%s)
local duration=$((end_time - start_time))
print_summary
echo -e "${SPARKLES} ${BOLD}Coverage Complete${RESET}"
echo -e "Duration: ${YELLOW}${duration}s${RESET}\n"
open_reports "$1"
}
main "$@"
+21 -5
View File
@@ -2,6 +2,9 @@
set -o pipefail
# PHP binary to use (defaults to PHP 8.2)
PHP_BINARY="${PHP_BINARY:-/opt/homebrew/opt/php@8.2/bin/php}"
# Colors and styles
readonly RESET='\033[0m'
readonly BOLD='\033[1m'
@@ -38,7 +41,7 @@ print_header() {
run_rector() {
echo -e "${BOLD}${SEARCH} Running Rector${RESET}"
if php vendor/bin/rector; then
if $PHP_BINARY vendor/bin/rector; then
echo -e "${CHECK} ${GREEN}Rector completed successfully${RESET}\n"
return 0
else
@@ -50,7 +53,7 @@ run_rector() {
run_php_cs_fixer() {
echo -e "${BOLD}${PALETTE} Running PHP-CS-Fixer${RESET}"
if php vendor/bin/php-cs-fixer fix -v; then
if $PHP_BINARY vendor/bin/php-cs-fixer fix -v; then
echo -e "${CHECK} ${GREEN}PHP-CS-Fixer completed successfully${RESET}\n"
return 0
else
@@ -62,7 +65,7 @@ run_php_cs_fixer() {
run_phpstan() {
echo -e "${BOLD}${MICROSCOPE} Running PHPStan${RESET}"
if php vendor/bin/phpstan analyse --memory-limit=-1; then
if $PHP_BINARY vendor/bin/phpstan analyse --memory-limit=-1; then
echo -e "${CHECK} ${GREEN}PHPStan analysis completed successfully${RESET}\n"
return 0
else
@@ -101,7 +104,7 @@ validate_composer_files() {
run_phplint() {
echo -e "${BOLD}${MAGNIFIER} Running PHPLint${RESET}"
if php vendor/bin/phplint; then
if $PHP_BINARY vendor/bin/phplint; then
echo -e "${CHECK} ${GREEN}PHPLint completed successfully${RESET}\n"
return 0
else
@@ -113,7 +116,7 @@ run_phplint() {
run_phpunit() {
echo -e "${BOLD}${TEST_TUBE} Running PHPUnit Tests${RESET}"
if php vendor/bin/phpunit; then
if $PHP_BINARY vendor/bin/phpunit; then
echo -e "${CHECK} ${GREEN}All tests passed successfully${RESET}\n"
return 0
else
@@ -122,6 +125,18 @@ run_phpunit() {
fi
}
run_vitest() {
echo -e "${BOLD}${TEST_TUBE} Running Vitest Tests${RESET}"
if npm run test -- --run; then
echo -e "${CHECK} ${GREEN}Vitest tests passed successfully${RESET}\n"
return 0
else
echo -e "${WARNING} ${YELLOW}Vitest tests failed${RESET}\n"
return 1
fi
}
main() {
local start_time=$(date +%s)
local issues_found=false
@@ -135,6 +150,7 @@ main() {
validate_composer_files || issues_found=true
run_phplint || issues_found=true
run_phpunit || issues_found=true
run_vitest || issues_found=true
local end_time=$(date +%s)
local duration=$((end_time - start_time))
+3 -3
View File
@@ -22,9 +22,9 @@
"require": {
"php": ">=8.2",
"ext-intl": "*",
"illuminate/contracts": "^11.0|^12.0",
"illuminate/routing": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"illuminate/contracts": "^11.0|^12.0|^13.0",
"illuminate/routing": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"laravel/octane": "^2.3",
"livewire/livewire": "^3.0",
"paragonie/random_compat": "^2.0",
Generated
+364 -295
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"dist/main.css": "/dist/main.388db426.css",
"dist/main.js": "/dist/main.943ce8e8.js",
"dist/main.css": "/dist/main.b2cdcaf6.css",
"dist/main.js": "/dist/main.29f69f71.js",
"dist/455.3a7b4474.css": "/dist/455.3a7b4474.css",
"dist/455.095e6545.js": "/dist/455.095e6545.js",
"dist/411.29cd993e.css": "/dist/411.29cd993e.css",
+2
View File
@@ -1,5 +1,6 @@
<i class="fa-duotone fa-rocket"></i> Getting Started:
<i class="fa-duotone fa-book text-indigo-900 mr-1 fa-lg"></i> Introduction: '/'
<i class="fa-duotone fa-flask text-purple-600 mr-1 fa-lg"></i> Playground: '/playground/'
<i class="fa-brands fa-symfony fa-lg text-black mr-1"></i> Symfony: '/symfony/'
<i class="fa-brands fa-laravel fa-lg text-red-900 mr-1"></i> Laravel: '/laravel/'
<i class="fa-duotone fa-ghost fa-lg text-pink-800 mr-1"></i> Livewire: '/livewire/'
@@ -12,6 +13,7 @@
<i class="fa-duotone fa-bread-loaf text-indigo-900 mr-1 fa-lg"></i> Toastr: '/library/toastr/'
<i class="fa-duotone fa-palette"></i> Themes <span class="text-xs bg-indigo-500 text-white px-2 py-0.5 rounded-full font-medium ml-1">NEW</span>:
<i class="fa-duotone fa-th-large text-indigo-600 mr-1 fa-lg"></i> All Themes: '/themes/'
<i class="fa-duotone fa-circle-minus text-indigo-900 mr-1 fa-lg"></i> Minimal: '/theme/minimal/'
<i class="fa-duotone fa-sun text-yellow-600 mr-1 fa-lg"></i> Amber: '/theme/amber/'
<i class="fa-duotone fa-diamond text-indigo-900 mr-1 fa-lg"></i> Crystal: '/theme/crystal/'
@@ -15,6 +15,12 @@ export default class extends Controller {
createAnchorNavigation() {
const ul = this.container.querySelector('ul')
// Guard against missing ul element
if (!ul) {
return
}
const anchors = document.querySelectorAll('#main-article h3, #main-article h2, #main-article a.anchor')
if (anchors.length === 0) {
@@ -20,7 +20,7 @@ export default class extends Controller {
parent.append(button)
button.addEventListener('click', () => {
button.addEventListener('click', async () => {
let code = codeBlock.textContent.trim()
if (code.startsWith('#')) {
const parts = code.split('\n')
@@ -28,9 +28,19 @@ export default class extends Controller {
code = parts.join('\n')
}
window.navigator.clipboard.writeText(code)
// Check if clipboard API is available
if (!window.navigator?.clipboard?.writeText) {
console.warn('Clipboard API not available')
return
}
button.innerHTML = '<i class="fa-duotone fa-clipboard-check"></i>'
try {
await window.navigator.clipboard.writeText(code)
button.innerHTML = '<i class="fa-duotone fa-clipboard-check"></i>'
} catch (error) {
console.error('Failed to copy to clipboard:', error)
button.innerHTML = '<i class="fa-duotone fa-clipboard-question"></i>'
}
setTimeout(() => {
button.innerHTML = icon
@@ -4,6 +4,12 @@ export default class extends Controller {
connect() {
const prevNext = document.querySelectorAll('.prev-next')
const navigation = document.getElementById('main-navigation')
// Guard against missing navigation element
if (!navigation) {
return
}
const navigationLinks = navigation.querySelectorAll('a')
let previous
@@ -15,6 +21,10 @@ export default class extends Controller {
links.forEach((link) => {
const label = link.querySelector('span')
// Guard against missing span element
if (!label) {
return
}
label.innerHTML = originalLink.innerHTML.replace(/\d+\. /, '').replace(/<(\S*?)[^>]*>.*?<\/\1>|<.*?\/>/, '')
link.href = originalLink.href
link.classList.remove('hidden')
+2 -2
View File
@@ -2,10 +2,10 @@
"entrypoints": {
"main": {
"css": [
"/dist/main.388db426.css"
"/dist/main.b2cdcaf6.css"
],
"js": [
"/dist/main.943ce8e8.js"
"/dist/main.29f69f71.js"
]
}
}
+2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+205
View File
@@ -444,6 +444,211 @@ class UserComponent extends Component
</div>
</div>
<h3 class="text-xl font-semibold text-slate-700 mt-6 mb-3">Toastr Events</h3>
<p class="mb-4">
For Toastr notifications, you can listen to the following events:
</p>
<div class="overflow-x-auto mb-6">
<table class="min-w-full bg-white border border-slate-200 rounded-lg">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Event</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">toastr:click</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when user clicks the notification</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">toastr:close</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when notification is closed by user</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">toastr:show</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when notification is shown</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">toastr:hidden</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when notification is hidden</td>
</tr>
</tbody>
</table>
</div>
<!-- Toastr Example -->
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-slate-200 mb-6">
<div class="bg-slate-800 px-4 py-3 flex items-center">
<div class="flex space-x-1.5 mr-3">
<div class="w-2.5 h-2.5 rounded-full bg-red-500"></div>
<div class="w-2.5 h-2.5 rounded-full bg-yellow-500"></div>
<div class="w-2.5 h-2.5 rounded-full bg-green-500"></div>
</div>
<div class="text-white text-sm">ToastrEventsComponent.php</div>
</div>
<div>
<pre class="bg-slate-50 rounded-lg p-4 text-sm overflow-x-auto"><code class="language-php">&lt;?php
namespace App\Livewire;
use Livewire\Attributes\On;
use Livewire\Component;
class ToastrEventsComponent extends Component
{
#[On('toastr:click')]
public function onToastrClick(array $payload): void
{
// Handle notification click
}
#[On('toastr:close')]
public function onToastrClose(array $payload): void
{
// Handle notification close
}
#[On('toastr:show')]
public function onToastrShow(array $payload): void
{
// Handle notification shown
}
#[On('toastr:hidden')]
public function onToastrHidden(array $payload): void
{
// Handle notification hidden
}
}</code></pre>
</div>
</div>
<h3 class="text-xl font-semibold text-slate-700 mt-6 mb-3">Noty Events</h3>
<p class="mb-4">
For Noty notifications, you can listen to the following events:
</p>
<div class="overflow-x-auto mb-6">
<table class="min-w-full bg-white border border-slate-200 rounded-lg">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Event</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">noty:click</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when user clicks the notification</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">noty:close</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when notification is closed</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">noty:show</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when notification is shown</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">noty:hover</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when user hovers over the notification</td>
</tr>
</tbody>
</table>
</div>
<h3 class="text-xl font-semibold text-slate-700 mt-6 mb-3">Notyf Events</h3>
<p class="mb-4">
For Notyf notifications, you can listen to the following events:
</p>
<div class="overflow-x-auto mb-6">
<table class="min-w-full bg-white border border-slate-200 rounded-lg">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Event</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">notyf:click</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when user clicks the notification</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">notyf:dismiss</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Fired when notification is dismissed</td>
</tr>
</tbody>
</table>
</div>
<h3 class="text-xl font-semibold text-slate-700 mt-6 mb-3">Theme Events</h3>
<p class="mb-4">
For PHPFlasher built-in themes, you can listen to the following events. Two types of events are dispatched:
a generic event and a theme-specific event.
</p>
<div class="overflow-x-auto mb-6">
<table class="min-w-full bg-white border border-slate-200 rounded-lg">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Event</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-slate-700 border-b">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">theme:click</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Generic event fired when any theme notification is clicked</td>
</tr>
<tr>
<td class="px-4 py-3"><code class="bg-slate-100 px-2 py-1 rounded text-sm">theme:{name}:click</code></td>
<td class="px-4 py-3 text-sm text-slate-600">Specific event fired for a particular theme (e.g., <code class="bg-slate-100 px-1 rounded text-xs">theme:flasher:click</code>)</td>
</tr>
</tbody>
</table>
</div>
<!-- Theme Example -->
<div class="bg-white rounded-xl shadow-sm overflow-hidden border border-slate-200 mb-6">
<div class="bg-slate-800 px-4 py-3 flex items-center">
<div class="flex space-x-1.5 mr-3">
<div class="w-2.5 h-2.5 rounded-full bg-red-500"></div>
<div class="w-2.5 h-2.5 rounded-full bg-yellow-500"></div>
<div class="w-2.5 h-2.5 rounded-full bg-green-500"></div>
</div>
<div class="text-white text-sm">ThemeEventsComponent.php</div>
</div>
<div>
<pre class="bg-slate-50 rounded-lg p-4 text-sm overflow-x-auto"><code class="language-php">&lt;?php
namespace App\Livewire;
use Livewire\Attributes\On;
use Livewire\Component;
class ThemeEventsComponent extends Component
{
// Listen to clicks on any theme notification
#[On('theme:click')]
public function onThemeClick(array $payload): void
{
// Handle notification click
}
// Listen to clicks on a specific theme (e.g., 'flasher')
#[On('theme:flasher:click')]
public function onFlasherThemeClick(array $payload): void
{
// Handle flasher theme notification click
}
}</code></pre>
</div>
</div>
<h3 class="text-xl font-semibold text-slate-700 mt-6 mb-3">Event Payload</h3>
<p class="mb-4">Each listener method accepts an <code class="bg-slate-200 px-2 py-1 rounded text-slate-700">array $payload</code> parameter, which contains:</p>
+223
View File
@@ -0,0 +1,223 @@
---
permalink: /playground/
title: Interactive Playground
description: Try PHPFlasher notifications live in your browser. Experiment with different types, themes, positions, and options - no installation required.
---
<div class="container max-w-7xl mx-auto px-4 lg:px-6 py-4">
<!-- Quick Stats -->
<section class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 text-center hover:shadow-md transition-shadow">
<div class="text-3xl font-bold text-indigo-600 mb-1">4</div>
<div class="text-slate-600 text-sm">Notification Types</div>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 text-center hover:shadow-md transition-shadow">
<div class="text-3xl font-bold text-indigo-600 mb-1">26</div>
<div class="text-slate-600 text-sm">Beautiful Themes</div>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 text-center hover:shadow-md transition-shadow">
<div class="text-3xl font-bold text-indigo-600 mb-1">6</div>
<div class="text-slate-600 text-sm">Position Options</div>
</div>
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 text-center hover:shadow-md transition-shadow">
<div class="text-3xl font-bold text-indigo-600 mb-1">5</div>
<div class="text-slate-600 text-sm">Adapters</div>
</div>
</section>
<!-- Interactive Studio -->
<section id="studio" class="scroll-mt-8">
{% include flasher-studio.html %}
</section>
<!-- Theme Quick Select -->
<section class="mt-16 mb-12">
<div class="text-center mb-8">
<div class="inline-block px-3 py-1 bg-indigo-50 text-indigo-700 rounded-full text-sm font-medium mb-3">
Quick Theme Preview
</div>
<h2 class="text-2xl md:text-3xl font-bold text-slate-800 mb-2">Popular Themes</h2>
<p class="text-slate-600">Click any theme to see it in action</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3" id="theme-quick-select">
<!-- Theme buttons will trigger notifications -->
<button onclick="showThemeDemo('flasher')" class="group relative overflow-hidden bg-gradient-to-br from-indigo-500 to-indigo-600 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-bolt"></i></div>
<div class="font-medium text-sm">Flasher</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('material')" class="group relative overflow-hidden bg-gradient-to-br from-blue-500 to-blue-600 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fab fa-google"></i></div>
<div class="font-medium text-sm">Material</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('ios')" class="group relative overflow-hidden bg-gradient-to-br from-slate-600 to-slate-700 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fab fa-apple"></i></div>
<div class="font-medium text-sm">iOS</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('slack')" class="group relative overflow-hidden bg-gradient-to-br from-purple-500 to-purple-600 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fab fa-slack"></i></div>
<div class="font-medium text-sm">Slack</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('amazon')" class="group relative overflow-hidden bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fab fa-amazon"></i></div>
<div class="font-medium text-sm">Amazon</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('minimal')" class="group relative overflow-hidden bg-gradient-to-br from-slate-400 to-slate-500 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-minus"></i></div>
<div class="font-medium text-sm">Minimal</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('neon')" class="group relative overflow-hidden bg-gradient-to-br from-pink-500 to-rose-500 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-lightbulb"></i></div>
<div class="font-medium text-sm">Neon</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('emerald')" class="group relative overflow-hidden bg-gradient-to-br from-emerald-500 to-emerald-600 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-gem"></i></div>
<div class="font-medium text-sm">Emerald</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('sapphire')" class="group relative overflow-hidden bg-gradient-to-br from-blue-600 to-indigo-600 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-gem"></i></div>
<div class="font-medium text-sm">Sapphire</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('ruby')" class="group relative overflow-hidden bg-gradient-to-br from-red-500 to-red-600 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-gem"></i></div>
<div class="font-medium text-sm">Ruby</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('onyx')" class="group relative overflow-hidden bg-gradient-to-br from-slate-800 to-slate-900 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-moon"></i></div>
<div class="font-medium text-sm">Onyx</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
<button onclick="showThemeDemo('aurora')" class="group relative overflow-hidden bg-gradient-to-br from-green-400 via-blue-500 to-purple-500 text-white rounded-xl p-4 text-center hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<div class="text-2xl mb-2"><i class="fas fa-sun"></i></div>
<div class="font-medium text-sm">Aurora</div>
<div class="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</button>
</div>
<div class="text-center mt-6">
<a href="/themes/" class="inline-flex items-center text-indigo-600 hover:text-indigo-700 font-medium">
View all 26 themes
<i class="fas fa-arrow-right ml-2"></i>
</a>
</div>
</section>
<!-- Keyboard Shortcuts -->
<section class="bg-slate-50 rounded-2xl p-8 mb-12">
<div class="text-center mb-6">
<h3 class="text-xl font-bold text-slate-800 mb-2">
<i class="fas fa-keyboard text-indigo-500 mr-2"></i>
Pro Tips
</h3>
<p class="text-slate-600">Make the most of the playground</p>
</div>
<div class="grid md:grid-cols-3 gap-6 max-w-4xl mx-auto">
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center mb-3">
<div class="w-10 h-10 bg-emerald-100 text-emerald-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-bolt"></i>
</div>
<h4 class="font-semibold text-slate-800">Quick Presets</h4>
</div>
<p class="text-slate-600 text-sm">Use the preset buttons for common notification scenarios like "User Created" or "Payment Failed".</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center mb-3">
<div class="w-10 h-10 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-code"></i>
</div>
<h4 class="font-semibold text-slate-800">Copy Code</h4>
</div>
<p class="text-slate-600 text-sm">The code panel updates in real-time. Switch between Laravel, Symfony, and JavaScript tabs.</p>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center mb-3">
<div class="w-10 h-10 bg-purple-100 text-purple-600 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-gamepad"></i>
</div>
<h4 class="font-semibold text-slate-800">Easter Egg</h4>
</div>
<p class="text-slate-600 text-sm">Try the Konami code for a surprise! <span class="text-xs text-slate-400">(Hint: it's a classic)</span></p>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-8 md:p-12 text-center text-white">
<h2 class="text-2xl md:text-3xl font-bold mb-4">Ready to use PHPFlasher?</h2>
<p class="text-indigo-100 mb-8 max-w-2xl mx-auto">
Install PHPFlasher in your Laravel or Symfony project and start showing beautiful notifications in minutes.
</p>
<div class="flex flex-wrap justify-center gap-4">
<a href="/laravel/" class="inline-flex items-center px-6 py-3 bg-white text-indigo-600 font-semibold rounded-xl hover:bg-indigo-50 transition-colors shadow-lg">
<i class="fab fa-laravel mr-2 text-red-500"></i>
Laravel Guide
</a>
<a href="/symfony/" class="inline-flex items-center px-6 py-3 bg-white/10 backdrop-blur-sm text-white font-semibold rounded-xl border border-white/30 hover:bg-white/20 transition-colors">
<i class="fab fa-symfony mr-2"></i>
Symfony Guide
</a>
</div>
</section>
</div>
<script>
// Theme demo function
function showThemeDemo(themeName) {
if (typeof flasher !== 'undefined') {
const messages = {
success: 'Operation completed successfully!',
info: 'Here is some useful information.',
warning: 'Please review before continuing.',
error: 'Something went wrong!'
};
const types = ['success', 'info', 'warning', 'error'];
const randomType = types[Math.floor(Math.random() * types.length)];
flasher.use(`theme.${themeName}`).flash({
type: randomType,
message: messages[randomType],
title: themeName.charAt(0).toUpperCase() + themeName.slice(1) + ' Theme'
});
}
}
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
</script>
-213
View File
@@ -1,213 +0,0 @@
# PHPFlasher Theme System
## Overview
The PHPFlasher theme system allows for consistent, customizable notification designs across your application. Themes define both the visual appearance (CSS/SCSS) and HTML structure of notifications.
## Core Concepts
### Theme Structure
Each theme consists of:
1. **SCSS file**: Defines the styling for the notification
2. **TypeScript file**: Contains the `render` function that generates HTML
3. **Index file**: Registers the theme with PHPFlasher
### Theme Registration
Themes are registered using the `addTheme` method:
```typescript
flasher.addTheme('themeName', myTheme);
```
### Using Themes
Themes can be used in two ways:
1. **Direct Usage**: Using the theme as a plugin
```typescript
flasher.use('theme.material').success('Operation completed');
```
2. **Default Theme**: Setting a theme as the default
```typescript
// Make material theme the default
const defaultTheme = 'theme.material';
flasher.defaultPlugin = defaultTheme;
```
## Theme Components
### CSS Variables
PHPFlasher uses CSS variables to maintain consistent styling across themes. The main variables are:
```css
:root {
/* State Colors */
--fl-success: #10b981;
--fl-info: #3b82f6;
--fl-warning: #f59e0b;
--fl-error: #ef4444;
/* Base colors */
--fl-bg-light: #ffffff;
--fl-bg-dark: rgb(15, 23, 42);
--fl-text-light: rgb(75, 85, 99);
--fl-text-dark: #ffffff;
/* Appearance */
--fl-font: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--fl-border-radius: 4px;
--fl-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
```
### Layout Components
1. **Wrapper** (`fl-wrapper`): Controls positioning and stacking of notifications
2. **Container** (`fl-container`): Base for individual notifications
3. **Progress Bar** (`fl-progress-bar`): Shows time remaining before auto-dismiss
4. **Icons**: Type-specific icons for visual recognition
### SASS Mixins
PHPFlasher provides several mixins to help with theme creation:
1. **variants**: Apply styles to each notification type
```scss
@include variants using($type) {
background-color: var(--#{$type}-color);
}
```
2. **close-button**: Standard close button styling
```scss
@include close-button;
```
3. **rtl-support**: Right-to-left language support
```scss
@include rtl-support;
```
4. **progress-bar**: Add a progress bar with type-specific colors
```scss
@include progress-bar(0.125em);
```
5. **dark-mode**: Dark mode styling support
```scss
@include dark-mode {
background-color: var(--fl-bg-dark);
color: var(--fl-text-dark);
}
```
## Creating Custom Themes
### Basic Theme Structure
```typescript
// my-theme.ts
import './my-theme.scss'
import type { Envelope } from '@flasher/flasher/dist/types'
export const myTheme = {
render: (envelope: Envelope): string => {
const { type, title, message } = envelope
return `
<div class="fl-my-theme fl-${type}">
<div class="fl-content">
<strong>${title || type}</strong>
<p>${message}</p>
<button class="fl-close">&times;</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
</div>
</div>
`
},
// Optional: CSS stylesheets to load
styles: ['path/to/additional-styles.css']
}
```
### SCSS Template
```scss
// my-theme.scss
@use '../mixins' as *;
.fl-my-theme {
padding: 1rem;
background-color: white;
border-radius: 4px;
box-shadow: var(--fl-shadow);
.fl-content {
display: flex;
align-items: center;
}
// Use built-in mixins for common functionality
@include close-button;
@include rtl-support;
@include progress-bar;
// Type-specific styling
@include variants using($type) {
border-left: 3px solid var(--#{$type}-color);
}
// Dark mode support
@include dark-mode {
background-color: var(--fl-bg-dark);
color: var(--fl-text-dark);
}
}
```
### Theme Registration
```typescript
// Register your custom theme
import { myTheme } from './my-theme';
flasher.addTheme('custom', myTheme);
// Use your theme
flasher.use('theme.custom').success('This uses my custom theme!');
```
## Included Themes
PHPFlasher comes with several built-in themes:
- **flasher**: Default bordered notification with colored accents (default)
- **material**: Google Material Design inspired notifications
- **bootstrap**: Bootstrap-styled notifications
- **tailwind**: Tailwind CSS styled notifications
- And many more...
## Accessibility Features
PHPFlasher themes include several accessibility features:
1. **ARIA roles**: Appropriate roles (`alert` or `status`) based on type
2. **ARIA live regions**: Assertive for critical messages, polite for others
3. **Reduced motion**: Respects user preferences for reduced animations
4. **RTL support**: Right-to-left language direction support
5. **Keyboard interaction**: Close buttons are keyboard accessible
## Best Practices
1. **Use semantic HTML** in your theme's render function
2. **Leverage CSS variables** for consistent styling
3. **Include proper ARIA attributes** for accessibility
4. **Use the provided mixins** to ensure consistent behavior
5. **Test in both light and dark modes** using the dark mode mixin
+403
View File
@@ -0,0 +1,403 @@
---
permalink: /themes/
title: Theme Gallery
description: Explore 26 beautiful notification themes for PHPFlasher. From minimal to material design, iOS to Slack-style - find the perfect look for your application.
---
<div class="container max-w-7xl mx-auto px-4 lg:px-6 py-4">
<!-- Try All Themes Section -->
<section class="bg-white rounded-2xl border border-slate-200 p-8 mb-8">
<div class="text-center mb-6">
<h2 class="text-2xl font-bold text-slate-800 mb-2">Try All Themes</h2>
<p class="text-slate-600">Click any button to see the theme in action</p>
</div>
<div class="flex flex-wrap justify-center gap-2" id="theme-demo-buttons">
<button onclick="demoTheme('flasher')" class="px-4 py-2 bg-indigo-100 hover:bg-indigo-200 text-indigo-700 rounded-lg text-sm font-medium transition-colors">Flasher</button>
<button onclick="demoTheme('material')" class="px-4 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg text-sm font-medium transition-colors">Material</button>
<button onclick="demoTheme('ios')" class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-sm font-medium transition-colors">iOS</button>
<button onclick="demoTheme('slack')" class="px-4 py-2 bg-purple-100 hover:bg-purple-200 text-purple-700 rounded-lg text-sm font-medium transition-colors">Slack</button>
<button onclick="demoTheme('amazon')" class="px-4 py-2 bg-orange-100 hover:bg-orange-200 text-orange-700 rounded-lg text-sm font-medium transition-colors">Amazon</button>
<button onclick="demoTheme('facebook')" class="px-4 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg text-sm font-medium transition-colors">Facebook</button>
<button onclick="demoTheme('google')" class="px-4 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg text-sm font-medium transition-colors">Google</button>
<button onclick="demoTheme('minimal')" class="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-sm font-medium transition-colors">Minimal</button>
<button onclick="demoTheme('neon')" class="px-4 py-2 bg-pink-100 hover:bg-pink-200 text-pink-700 rounded-lg text-sm font-medium transition-colors">Neon</button>
<button onclick="demoTheme('ruby')" class="px-4 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg text-sm font-medium transition-colors">Ruby</button>
<button onclick="demoTheme('sapphire')" class="px-4 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg text-sm font-medium transition-colors">Sapphire</button>
<button onclick="demoTheme('emerald')" class="px-4 py-2 bg-emerald-100 hover:bg-emerald-200 text-emerald-700 rounded-lg text-sm font-medium transition-colors">Emerald</button>
<button onclick="demoTheme('jade')" class="px-4 py-2 bg-teal-100 hover:bg-teal-200 text-teal-700 rounded-lg text-sm font-medium transition-colors">Jade</button>
<button onclick="demoTheme('amber')" class="px-4 py-2 bg-amber-100 hover:bg-amber-200 text-amber-700 rounded-lg text-sm font-medium transition-colors">Amber</button>
<button onclick="demoTheme('onyx')" class="px-4 py-2 bg-slate-700 hover:bg-slate-800 text-white rounded-lg text-sm font-medium transition-colors">Onyx</button>
<button onclick="demoTheme('crystal')" class="px-4 py-2 bg-cyan-100 hover:bg-cyan-200 text-cyan-700 rounded-lg text-sm font-medium transition-colors">Crystal</button>
<button onclick="demoTheme('aurora')" class="px-4 py-2 bg-gradient-to-r from-green-100 to-purple-100 hover:from-green-200 hover:to-purple-200 text-purple-700 rounded-lg text-sm font-medium transition-colors">Aurora</button>
</div>
</section>
<!-- Theme Categories -->
<section id="themes">
<!-- Brand-Inspired Themes -->
<div class="mb-12">
<div class="flex items-center mb-6">
<div class="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-building text-indigo-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-slate-800">Brand-Inspired</h2>
<p class="text-sm text-slate-500">Familiar styles from popular platforms</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Material -->
<a href="/theme/material/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center">
<i class="fab fa-google text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Material</h3>
<p class="text-sm text-slate-500">Google Material Design</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- iOS -->
<a href="/theme/ios/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-slate-600 to-slate-800 flex items-center justify-center">
<i class="fab fa-apple text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">iOS</h3>
<p class="text-sm text-slate-500">Apple iOS notifications</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Slack -->
<a href="/theme/slack/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-purple-500 to-purple-700 flex items-center justify-center">
<i class="fab fa-slack text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Slack</h3>
<p class="text-sm text-slate-500">Slack messaging style</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Amazon -->
<a href="/theme/amazon/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center">
<i class="fab fa-amazon text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Amazon</h3>
<p class="text-sm text-slate-500">Amazon-inspired alerts</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Facebook -->
<a href="/theme/facebook/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center">
<i class="fab fa-facebook text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Facebook</h3>
<p class="text-sm text-slate-500">Facebook notification style</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Google -->
<a href="/theme/google/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-red-500 via-yellow-500 to-green-500 flex items-center justify-center">
<i class="fab fa-google text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Google</h3>
<p class="text-sm text-slate-500">Google-style notifications</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
</div>
</div>
<!-- Gemstone Themes -->
<div class="mb-12">
<div class="flex items-center mb-6">
<div class="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-gem text-purple-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-slate-800">Gemstone Collection</h2>
<p class="text-sm text-slate-500">Elegant color palettes inspired by precious stones</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Ruby -->
<a href="/theme/ruby/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-red-500 to-rose-600 flex items-center justify-center">
<i class="fas fa-gem text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Ruby</h3>
<p class="text-sm text-slate-500">Bold ruby red accents</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Sapphire -->
<a href="/theme/sapphire/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center">
<i class="fas fa-gem text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Sapphire</h3>
<p class="text-sm text-slate-500">Elegant blue elegance</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Emerald -->
<a href="/theme/emerald/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-emerald-500 to-green-600 flex items-center justify-center">
<i class="fas fa-gem text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Emerald</h3>
<p class="text-sm text-slate-500">Modern green palette</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Jade -->
<a href="/theme/jade/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-teal-500 to-green-600 flex items-center justify-center">
<i class="fas fa-leaf text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Jade</h3>
<p class="text-sm text-slate-500">Soft jade colors</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Amber -->
<a href="/theme/amber/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center">
<i class="fas fa-sun text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Amber</h3>
<p class="text-sm text-slate-500">Warm amber tones</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Onyx -->
<a href="/theme/onyx/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-slate-700 to-slate-900 flex items-center justify-center">
<i class="fas fa-moon text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Onyx</h3>
<p class="text-sm text-slate-500">Dark mode sleek</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Crystal -->
<a href="/theme/crystal/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-cyan-400 to-blue-500 flex items-center justify-center">
<i class="fas fa-snowflake text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Crystal</h3>
<p class="text-sm text-slate-500">Transparent design</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Aurora -->
<a href="/theme/aurora/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-green-400 via-blue-500 to-purple-500 flex items-center justify-center">
<i class="fas fa-sparkles text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Aurora</h3>
<p class="text-sm text-slate-500">Gradient effects</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
</div>
</div>
<!-- Minimal & Classic Themes -->
<div class="mb-12">
<div class="flex items-center mb-6">
<div class="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-minus text-slate-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-slate-800">Minimal & Classic</h2>
<p class="text-sm text-slate-500">Clean, simple, and versatile designs</p>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Flasher (Default) -->
<a href="/theme/flasher/" class="group relative bg-white rounded-2xl border-2 border-indigo-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="absolute top-3 left-3 px-2 py-1 bg-indigo-500 text-white text-xs font-medium rounded-full">Default</div>
<div class="h-24 bg-gradient-to-br from-indigo-500 to-indigo-600 flex items-center justify-center">
<i class="fas fa-bolt text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Flasher</h3>
<p class="text-sm text-slate-500">Default clean design</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Minimal -->
<a href="/theme/minimal/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-slate-400 to-slate-500 flex items-center justify-center">
<i class="fas fa-minus text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Minimal</h3>
<p class="text-sm text-slate-500">Ultra-clean and simple</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
<!-- Neon -->
<a href="/theme/neon/" class="group relative bg-white rounded-2xl border border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<div class="h-24 bg-gradient-to-br from-pink-500 to-rose-600 flex items-center justify-center">
<i class="fas fa-lightbulb text-4xl text-white/80"></i>
</div>
<div class="p-4">
<h3 class="font-semibold text-slate-800 mb-1">Neon</h3>
<p class="text-sm text-slate-500">Bright attention-grabbing</p>
</div>
<div class="absolute top-3 right-3 w-8 h-8 bg-white/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="fas fa-arrow-right text-white text-sm"></i>
</div>
</a>
</div>
</div>
</section>
<!-- Usage Section -->
<section class="bg-slate-50 rounded-2xl p-8 mb-12">
<div class="max-w-3xl mx-auto">
<h2 class="text-2xl font-bold text-slate-800 mb-6 text-center">How to Use Themes</h2>
<div class="space-y-6">
<div class="bg-white rounded-xl p-6 shadow-sm">
<div class="flex items-center mb-4">
<div class="w-8 h-8 bg-indigo-100 rounded-lg flex items-center justify-center mr-3">
<span class="text-indigo-600 font-bold">1</span>
</div>
<h3 class="font-semibold text-slate-800">Set as Default Theme</h3>
</div>
<pre class="bg-slate-800 text-slate-200 p-4 rounded-lg text-sm overflow-x-auto"><code>// config/flasher.php (Laravel)
return [
'default' => 'theme.material',
];</code></pre>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm">
<div class="flex items-center mb-4">
<div class="w-8 h-8 bg-indigo-100 rounded-lg flex items-center justify-center mr-3">
<span class="text-indigo-600 font-bold">2</span>
</div>
<h3 class="font-semibold text-slate-800">Use Per-Notification</h3>
</div>
<pre class="bg-slate-800 text-slate-200 p-4 rounded-lg text-sm overflow-x-auto"><code>// PHP
flash()->use('theme.material')->success('Operation completed!');
// JavaScript
flasher.use('theme.material').success('Operation completed!');</code></pre>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-8 md:p-12 text-center text-white">
<h2 class="text-2xl md:text-3xl font-bold mb-4">Can't decide?</h2>
<p class="text-indigo-100 mb-8 max-w-2xl mx-auto">
Try all themes in our interactive playground and see which one fits your project best.
</p>
<a href="/playground/" class="inline-flex items-center px-8 py-4 bg-white text-indigo-600 font-semibold rounded-xl hover:bg-indigo-50 transition-colors shadow-lg">
<i class="fas fa-flask mr-2"></i>
Open Playground
</a>
</section>
</div>
<script>
function demoTheme(themeName) {
if (typeof flasher !== 'undefined') {
const types = ['success', 'info', 'warning', 'error'];
const messages = {
success: 'Operation completed successfully!',
info: 'Here is some useful information.',
warning: 'Please review before continuing.',
error: 'Something went wrong!'
};
const randomType = types[Math.floor(Math.random() * types.length)];
flasher.use(`theme.${themeName}`).flash({
type: randomType,
message: messages[randomType],
title: themeName.charAt(0).toUpperCase() + themeName.slice(1) + ' Theme'
});
}
}
// Smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
</script>
+2302 -306
View File
File diff suppressed because it is too large Load Diff
+21 -9
View File
@@ -41,12 +41,22 @@
"build": "cross-env NODE_ENV=production rollup -c",
"clean": "rimraf src/*/Prime/Resources/dist",
"link": "npm link --workspaces",
"ncu": "ncu -u && npm run ncu --workspaces"
"ncu": "ncu -u && npm run ncu --workspaces",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"coverage": "npm run coverage:php && npm run coverage:js",
"coverage:php": "/opt/homebrew/opt/php@8.2/bin/php vendor/bin/phpunit --coverage-html coverage/phpunit/html",
"coverage:js": "vitest run --coverage",
"coverage:open": "open coverage/vitest/index.html && open coverage/phpunit/html/index.html"
},
"devDependencies": {
"@antfu/eslint-config": "^2.27.3",
"@babel/core": "^7.28.6",
"@babel/preset-env": "^7.28.6",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^28.0.9",
"@rollup/plugin-eslint": "^9.2.0",
@@ -54,11 +64,11 @@
"@rollup/plugin-strip": "^3.0.4",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.0",
"@types/node": "^22.19.7",
"@types/node": "^22.19.13",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"all-contributors-cli": "^6.26.1",
"autoprefixer": "^10.4.23",
"autoprefixer": "^10.4.27",
"browserslist": "^4.28.1",
"cross-env": "^7.0.3",
"cssnano": "^7.1.2",
@@ -73,17 +83,19 @@
"postcss": "^8.5.6",
"postcss-discard-comments": "^7.0.5",
"punycode": "^2.3.1",
"rimraf": "^6.1.2",
"rollup": "^4.55.1",
"rimraf": "^6.1.3",
"rollup": "^4.59.0",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-filesize": "^10.0.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-progress": "^1.1.2",
"sass": "^1.97.2",
"sass": "^1.97.3",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^3.2.4",
"jsdom": "^26.1.0"
}
}
+106
View File
@@ -22,3 +22,109 @@ parameters:
ignoreErrors:
- '#Call to method .+ with .+ will always evaluate to true.#'
- '#Cannot call method (assertExitCode|expectsOutput|expectsOutputToContain|assertExitCode)\(\) on Illuminate\\Testing\\PendingCommand\|int\.#'
# Ignore "will always evaluate to false" assertions in tests
-
message: '#Call to method PHPUnit\\Framework\\Assert::assertInstanceOf\(\) with .+ will always evaluate to false.#'
path: tests/
-
message: '#Call to method PHPUnit\\Framework\\Assert::assertArrayHasKey\(\) with .+ will always evaluate to false.#'
path: tests/
# Ignore class-string issues in tests with ReflectionClass
-
message: '#Parameter \#1 \$objectOrClass of class ReflectionClass constructor expects class-string<T of object>\|T of object, string given.#'
path: tests/
# Ignore mixed return type issues in tests
-
message: '#Method .+::getFacadeAccessor\(\) should return string but returns mixed.#'
path: tests/
# Ignore offset issues in test assertions where we're testing dynamic data
-
message: '#Offset .+ does not exist on array\{.+\}\.#'
path: tests/
# Ignore method return type issues in test fixtures
-
message: '#Method class@anonymous.+::getFactory\(\) should return class-string.+#'
path: tests/
# Ignore missing value type in iterable issues in test fixtures
-
message: '#return type has no value type specified in iterable type array\.#'
path: tests/
-
message: '#has parameter .+ with no value type specified in iterable type array.#'
path: tests/
# Ignore undefined method issues in trait test helpers
-
message: '#Call to an undefined method object::.+#'
path: tests/
# Ignore parameter type issues in tests where we test edge cases
-
message: '#Parameter \#\d+ .+ expects .+, .+ given\.#'
path: tests/Laravel/Facade/
-
message: '#Parameter \#\d+ .+ expects .+, .+ given\.#'
path: tests/Noty/Prime/
-
message: '#Parameter \#\d+ .+ expects .+, .+ given\.#'
path: tests/Toastr/Prime/
# Ignore call to undefined static method in tests (testing facade methods)
-
message: '#Call to an undefined static method .+#'
path: tests/Laravel/Facade/
# Ignore offset issues in CSP tests
-
message: '#Offset mixed on array\{\}.+#'
path: tests/Prime/Http/Csp/
# Ignore method never returns issues in anonymous class mocks
-
message: '#Method class@anonymous.+never returns string so it can be removed from the return type\.#'
path: tests/
# Ignore cannot call method on null in tests
-
message: '#Cannot call method .+ on .+\|null\.#'
path: tests/
# Ignore issues with assertArrayHasKey in tests
-
message: '#Parameter \#2 \$(haystack|array) of method PHPUnit\\Framework\\Assert::.+ expects .+, mixed given\.#'
path: tests/
# Ignore json_decode issues in tests
-
message: '#Parameter \#1 \$json of function json_decode expects string, string\|false given\.#'
path: tests/
# Ignore Mockery-related issues in tests
-
message: '#Call to an undefined method Mockery\\ExpectationInterface.+#'
path: tests/
# Ignore VarDumper/Data accessor issues in Symfony profiler tests
-
message: '#Cannot call method getValue\(\) on array.+#'
path: tests/Symfony/Profiler/
-
message: '#Cannot access offset .+ on array.+#'
path: tests/Symfony/Profiler/
-
message: '#Cannot access offset .+ on mixed\.#'
path: tests/
# Ignore mixed parameter in assertions
-
message: '#Parameter \#\d+ .+ of method PHPUnit\\Framework\\Assert::.+ expects .+, mixed given\.#'
path: tests/
# Ignore property value type not specified in tests
-
message: '#Property .+ type has no value type specified in iterable type array\.#'
path: tests/
# Ignore "will always evaluate" issues in tests
-
message: '#Call to method PHPUnit\\Framework\\Assert::.+ with .+ will always evaluate to .+#'
path: tests/
# Ignore assertArrayHasKey with VarDumper types
-
message: '#Parameter \#2 \$array of method PHPUnit\\Framework\\Assert::assertArrayHasKey\(\) expects array\|ArrayAccess.+#'
path: tests/Symfony/Profiler/
# Ignore array_keys parameter with VarDumper types
-
message: '#Parameter \#1 \$array of function array_keys expects array.+#'
path: tests/Symfony/Profiler/
# Ignore config parameter type issues in tests
-
message: '#Parameter \#2 \$config of class Flasher\\Symfony\\Profiler\\FlasherDataCollector constructor expects.+#'
path: tests/Symfony/Profiler/
+15 -1
View File
@@ -46,11 +46,25 @@
</testsuite>
</testsuites>
<coverage/>
<coverage>
<report>
<html outputDirectory="coverage/phpunit/html"/>
<text outputFile="php://stdout" showUncoveredFiles="false"/>
</report>
</coverage>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<file>src/Prime/.phpstorm.meta.php</file>
<file>src/Laravel/.phpstorm.meta.php</file>
<file>src/Symfony/.phpstorm.meta.php</file>
<file>src/Noty/Prime/.phpstorm.meta.php</file>
<file>src/Notyf/Prime/.phpstorm.meta.php</file>
<file>src/SweetAlert/Prime/.phpstorm.meta.php</file>
<file>src/Toastr/Prime/.phpstorm.meta.php</file>
</exclude>
</source>
</phpunit>
+22 -2
View File
@@ -15,11 +15,31 @@ final class FlasherComponent extends Component
public function render(): string
{
/** @var array<string, mixed> $criteria */
$criteria = json_decode($this->criteria, true, 512, \JSON_THROW_ON_ERROR) ?: [];
$criteria = $this->decodeJson($this->criteria);
/** @var array<string, mixed> $context */
$context = json_decode($this->context, true, 512, \JSON_THROW_ON_ERROR) ?: [];
$context = $this->decodeJson($this->context);
return app('flasher')->render('html', $criteria, $context);
}
/**
* Safely decode JSON string, returning empty array on failure.
*
* @return array<string, mixed>
*/
private function decodeJson(string $json): array
{
if ('' === $json) {
return [];
}
try {
$decoded = json_decode($json, true, 512, \JSON_THROW_ON_ERROR);
return \is_array($decoded) ? $decoded : [];
} catch (\JsonException) {
return [];
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ use Laravel\Octane\Events\RequestReceived;
final readonly class OctaneListener
{
public function handle(RequestReceived $event): void
public function __invoke(RequestReceived $event): void
{
/** @var NotificationLoggerListener $listener */
$listener = $event->sandbox->make('flasher.notification_logger_listener');
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Flasher\Laravel\EventListener;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface;
final readonly class ThemeLivewireListener implements EventListenerInterface
{
public function __invoke(ResponseEvent $event): void
{
// Only process HTML responses
if ('html' !== $event->getPresenter()) {
return;
}
$response = $event->getResponse() ?: '';
if (!\is_string($response)) {
return;
}
// Avoid duplicate script injection
if (false === strripos($response, '<script type="text/javascript" class="flasher-js"')) {
return;
}
if (strripos($response, '<script type="text/javascript" class="flasher-theme-livewire-js"')) {
return;
}
// Inject the Theme-Livewire bridge JavaScript
$response .= <<<'JAVASCRIPT'
<script type="text/javascript" class="flasher-theme-livewire-js">
(function() {
window.addEventListener('flasher:theme:click', function(event) {
if (typeof Livewire === 'undefined') {
return;
}
const { detail } = event;
const { envelope } = detail;
const context = envelope.context || {};
if (!context.livewire?.id) {
return;
}
const { livewire: { id: componentId } } = context;
const component = Livewire.all().find(c => c.id === componentId);
if (!component) {
return;
}
Livewire.dispatchTo(component.name, 'theme:click', { payload: detail });
// Also dispatch theme-specific event
const plugin = envelope.metadata?.plugin || '';
let themeName = plugin;
if (plugin.startsWith('theme.')) {
themeName = plugin.replace('theme.', '');
}
if (themeName) {
Livewire.dispatchTo(component.name, 'theme:' + themeName + ':click', { payload: detail });
}
}, false);
})();
</script>
JAVASCRIPT;
$event->setResponse($response);
}
public function getSubscribedEvents(): string
{
return ResponseEvent::class;
}
}
+15 -1
View File
@@ -8,6 +8,7 @@ use Flasher\Laravel\Command\InstallCommand;
use Flasher\Laravel\Component\FlasherComponent;
use Flasher\Laravel\EventListener\LivewireListener;
use Flasher\Laravel\EventListener\OctaneListener;
use Flasher\Laravel\EventListener\ThemeLivewireListener;
use Flasher\Laravel\Middleware\FlasherMiddleware;
use Flasher\Laravel\Middleware\SessionMiddleware;
use Flasher\Laravel\Storage\SessionBag;
@@ -17,6 +18,7 @@ use Flasher\Laravel\Translation\Translator;
use Flasher\Prime\Asset\AssetManager;
use Flasher\Prime\Container\FlasherContainer;
use Flasher\Prime\EventDispatcher\EventDispatcher;
use Flasher\Prime\EventDispatcher\EventDispatcherInterface;
use Flasher\Prime\EventDispatcher\EventListener\ApplyPresetListener;
use Flasher\Prime\EventDispatcher\EventListener\NotificationLoggerListener;
use Flasher\Prime\EventDispatcher\EventListener\TranslationListener;
@@ -66,6 +68,7 @@ final class FlasherServiceProvider extends PluginServiceProvider
$this->registerCommands();
$this->loadTranslationsFrom(__DIR__.'/Translation/lang', 'flasher');
$this->loadViewsFrom(__DIR__.'/Resources/views', 'flasher');
$this->registerMiddlewares();
$this->callAfterResolving('blade.compiler', $this->registerBladeDirectives(...));
$this->registerLivewire();
@@ -305,7 +308,7 @@ final class FlasherServiceProvider extends PluginServiceProvider
private function registerLivewire(): void
{
if (class_exists(LivewireManager::class) && !$this->app->bound('livewire')) {
if (!class_exists(LivewireManager::class) || !$this->app->bound('livewire')) {
return;
}
@@ -316,5 +319,16 @@ final class FlasherServiceProvider extends PluginServiceProvider
$livewire->listen('dehydrate', new LivewireListener($livewire, $flasher, $cspHandler, $request));
});
$this->registerThemeLivewireListener();
}
private function registerThemeLivewireListener(): void
{
$this->app->extend('flasher.event_dispatcher', static function (EventDispatcherInterface $dispatcher) {
$dispatcher->addListener(new ThemeLivewireListener());
return $dispatcher;
});
}
}
+4 -4
View File
@@ -56,14 +56,14 @@ final readonly class Request implements RequestInterface
{
$session = $this->getSession();
/** @var false|string|string[] $type */
$type = $session?->get($type);
/** @var false|string|string[] $value */
$value = $session?->get($type);
if (!\is_string($type) && !\is_array($type)) {
if (!\is_string($value) && !\is_array($value)) {
return [];
}
return $type;
return $value;
}
public function forgetType(string $type): void
@@ -0,0 +1,34 @@
@php
use Flasher\Prime\Notification\Envelope;
/** @var Envelope $envelope */
$type = $envelope->getType();
$message = $envelope->getMessage();
$alertClass = match($type) {
'success' => 'alert-success',
'error' => 'alert-danger',
'warning' => 'alert-warning',
default => 'alert-info',
};
$progressBgColor = match($type) {
'success' => '#155724',
'error' => '#721c24',
'warning' => '#856404',
default => '#0c5460',
};
@endphp
<div style="margin-top: 0.5rem;cursor: pointer;">
<div class="alert {{ $alertClass }} alert-dismissible fade in show" role="alert" style="border-top-left-radius: 0;border-bottom-left-radius: 0;border: unset;border-left: 6px solid {{ $progressBgColor }}">
{{ $message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="this.parentElement.remove()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="d-flex" style="height: .125rem;margin-top: -1rem;">
<span class="flasher-progress" style="background-color: {{ $progressBgColor }}"></span>
</div>
</div>
@@ -0,0 +1,66 @@
@php
use Flasher\Prime\Notification\Envelope;
/** @var Envelope $envelope */
$type = $envelope->getType();
$message = $envelope->getMessage();
$config = match($type) {
'success' => [
'title' => 'Success',
'text_color' => 'text-green-600',
'ring_color' => 'ring-green-300',
'background_color' => 'bg-green-600',
'progress_background_color' => 'bg-green-100',
'border_color' => 'border-green-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="check w-7 h-7"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
],
'error' => [
'title' => 'Error',
'text_color' => 'text-red-600',
'ring_color' => 'ring-red-300',
'background_color' => 'bg-red-600',
'progress_background_color' => 'bg-red-100',
'border_color' => 'border-red-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="x w-7 h-7"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
],
'warning' => [
'title' => 'Warning',
'text_color' => 'text-yellow-600',
'ring_color' => 'ring-yellow-300',
'background_color' => 'bg-yellow-600',
'progress_background_color' => 'bg-yellow-100',
'border_color' => 'border-yellow-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="exclamation w-7 h-7"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>',
],
default => [
'title' => 'Info',
'text_color' => 'text-blue-600',
'ring_color' => 'ring-blue-300',
'background_color' => 'bg-blue-600',
'progress_background_color' => 'bg-blue-100',
'border_color' => 'border-blue-600',
'icon' => '<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
],
};
@endphp
<div class="bg-white shadow-inner border-l-4 mt-2 cursor-pointer {{ $config['border_color'] }}">
<div class="flex items-center px-2 py-3 rounded-lg shadow-lg overflow-hidden">
<div class="inline-flex items-center {{ $config['background_color'] }} p-1 text-white text-sm rounded-full flex-shrink-0">
{!! $config['icon'] !!}
</div>
<div class="ml-4 w-0 flex-1">
<p class="text-base leading-5 font-medium capitalize {{ $config['text_color'] }}">
{{ $config['title'] }}
</p>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ $message }}
</p>
</div>
</div>
<div class="h-0.5 flex {{ $config['progress_background_color'] }}">
<span class="flasher-progress {{ $config['background_color'] }}"></span>
</div>
</div>
@@ -0,0 +1,62 @@
@php
use Flasher\Prime\Notification\Envelope;
/** @var Envelope $envelope */
$type = $envelope->getType();
$message = $envelope->getMessage();
$config = match($type) {
'success' => [
'title' => 'Success',
'text_color' => 'text-green-700',
'background_color' => 'bg-green-50',
'progress_background_color' => 'bg-green-200',
'border_color' => 'border-green-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="check w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
],
'error' => [
'title' => 'Error',
'text_color' => 'text-red-700',
'background_color' => 'bg-red-50',
'progress_background_color' => 'bg-red-200',
'border_color' => 'border-red-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="x w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
],
'warning' => [
'title' => 'Warning',
'text_color' => 'text-yellow-700',
'background_color' => 'bg-yellow-50',
'progress_background_color' => 'bg-yellow-200',
'border_color' => 'border-yellow-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="exclamation w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>',
],
default => [
'title' => 'Info',
'text_color' => 'text-blue-700',
'background_color' => 'bg-blue-50',
'progress_background_color' => 'bg-blue-200',
'border_color' => 'border-blue-600',
'icon' => '<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
],
};
@endphp
<div class="bg-white shadow-lg border-l-4 mt-2 cursor-pointer {{ $config['background_color'] }} {{ $config['border_color'] }}">
<div class="flex items-center px-2 py-3 rounded-lg shadow-lg overflow-hidden">
<div class="inline-flex items-center p-2 text-white text-sm rounded-full {{ $config['text_color'] }} flex-shrink-0">
{!! $config['icon'] !!}
</div>
<div class="ml-4 w-0 flex-1">
<p class="text-base leading-5 font-medium capitalize {{ $config['text_color'] }}">
{{ $config['title'] }}
</p>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ $message }}
</p>
</div>
</div>
<div class="h-0.5 flex {{ $config['background_color'] }}">
<span class="flasher-progress {{ $config['progress_background_color'] }}"></span>
</div>
</div>
@@ -0,0 +1,66 @@
@php
use Flasher\Prime\Notification\Envelope;
/** @var Envelope $envelope */
$type = $envelope->getType();
$message = $envelope->getMessage();
$config = match($type) {
'success' => [
'title' => 'Success',
'text_color' => 'text-green-600',
'ring_color' => 'ring-green-300',
'background_color' => 'bg-green-600',
'progress_background_color' => 'bg-green-100',
'border_color' => 'border-green-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="check w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
],
'error' => [
'title' => 'Error',
'text_color' => 'text-red-600',
'ring_color' => 'ring-red-300',
'background_color' => 'bg-red-600',
'progress_background_color' => 'bg-red-100',
'border_color' => 'border-red-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="x w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
],
'warning' => [
'title' => 'Warning',
'text_color' => 'text-yellow-600',
'ring_color' => 'ring-yellow-300',
'background_color' => 'bg-yellow-600',
'progress_background_color' => 'bg-yellow-100',
'border_color' => 'border-yellow-600',
'icon' => '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" class="exclamation w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>',
],
default => [
'title' => 'Info',
'text_color' => 'text-blue-600',
'ring_color' => 'ring-blue-300',
'background_color' => 'bg-blue-600',
'progress_background_color' => 'bg-blue-100',
'border_color' => 'border-blue-600',
'icon' => '<svg class="w-8 h-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>',
],
};
@endphp
<div class="bg-white shadow-inner border-l-4 mt-2 cursor-pointer {{ $config['border_color'] }}">
<div class="flex items-center px-2 py-3 rounded-lg shadow-lg overflow-hidden">
<div class="inline-flex items-center {{ $config['text_color'] }} p-1 text-xl rounded-full flex-shrink-0">
{!! $config['icon'] !!}
</div>
<div class="ml-4 w-0 flex-1">
<p class="text-base leading-5 font-medium capitalize {{ $config['text_color'] }}">
{{ $config['title'] }}
</p>
<p class="mt-1 text-sm leading-5 text-gray-500">
{{ $message }}
</p>
</div>
</div>
<div class="h-0.5 flex {{ $config['progress_background_color'] }}">
<span class="flasher-progress {{ $config['background_color'] }}"></span>
</div>
</div>
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Flasher\Laravel\Storage;
final class FallbackSession implements FallbackSessionInterface
{
/** @var array<string, mixed> */
private static array $storage = [];
public function get(string $name, mixed $default = null): mixed
{
return \array_key_exists($name, self::$storage) ? self::$storage[$name] : $default;
}
public function set(string $name, mixed $value): void
{
self::$storage[$name] = $value;
}
public static function reset(): void
{
self::$storage = [];
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Flasher\Laravel\Storage;
interface FallbackSessionInterface
{
public function get(string $name, mixed $default = null): mixed;
public function set(string $name, mixed $value): void;
}
+29 -3
View File
@@ -6,26 +6,52 @@ namespace Flasher\Laravel\Storage;
use Flasher\Prime\Notification\Envelope;
use Flasher\Prime\Storage\Bag\BagInterface;
use Illuminate\Contracts\Session\Session;
use Illuminate\Session\SessionManager;
use Illuminate\Session\Store;
final readonly class SessionBag implements BagInterface
{
public const ENVELOPES_NAMESPACE = 'flasher::envelopes';
public function __construct(private SessionManager $session)
private FallbackSessionInterface $fallbackSession;
public function __construct(private SessionManager $sessionManager, ?FallbackSessionInterface $fallbackSession = null)
{
$this->fallbackSession = $fallbackSession ?? new FallbackSession();
}
public function get(): array
{
$session = $this->getSession();
/** @var Envelope[] $envelopes */
$envelopes = $this->session->get(self::ENVELOPES_NAMESPACE, []);
$envelopes = $session->get(self::ENVELOPES_NAMESPACE, []);
return $envelopes;
}
public function set(array $envelopes): void
{
$this->session->put(self::ENVELOPES_NAMESPACE, $envelopes);
$session = $this->getSession();
if ($session instanceof FallbackSessionInterface) {
$session->set(self::ENVELOPES_NAMESPACE, $envelopes);
return;
}
$session->put(self::ENVELOPES_NAMESPACE, $envelopes);
}
private function getSession(): Session|FallbackSessionInterface
{
$session = $this->sessionManager->driver();
if ($session instanceof Store && $session->isStarted()) {
return $session;
}
return $this->fallbackSession;
}
}
+1 -1
View File
@@ -28,7 +28,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.2",
"illuminate/support": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"php-flasher/flasher": "^2.4.0"
},
"autoload": {
@@ -6,6 +6,7 @@ namespace Flasher\Noty\Laravel;
use Flasher\Laravel\Support\PluginServiceProvider;
use Flasher\Noty\Prime\NotyPlugin;
use Flasher\Prime\EventDispatcher\EventDispatcherInterface;
final class FlasherNotyServiceProvider extends PluginServiceProvider
{
@@ -13,4 +14,22 @@ final class FlasherNotyServiceProvider extends PluginServiceProvider
{
return new NotyPlugin();
}
protected function afterBoot(): void
{
$this->registerLivewireListener();
}
private function registerLivewireListener(): void
{
if (!$this->app->bound('livewire')) {
return;
}
$this->app->extend('flasher.event_dispatcher', static function (EventDispatcherInterface $dispatcher) {
$dispatcher->addListener(new LivewireListener());
return $dispatcher;
});
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Flasher\Noty\Laravel;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface;
final readonly class LivewireListener implements EventListenerInterface
{
public function __invoke(ResponseEvent $event): void
{
// Only process HTML responses
if ('html' !== $event->getPresenter()) {
return;
}
$response = $event->getResponse() ?: '';
if (!\is_string($response)) {
return;
}
// Avoid duplicate script injection
if (false === strripos($response, '<script type="text/javascript" class="flasher-js"')) {
return;
}
if (strripos($response, '<script type="text/javascript" class="flasher-noty-livewire-js"')) {
return;
}
// Inject the Noty-Livewire bridge JavaScript
$response .= <<<'JAVASCRIPT'
<script type="text/javascript" class="flasher-noty-livewire-js">
(function() {
const events = ['flasher:noty:show', 'flasher:noty:click', 'flasher:noty:close', 'flasher:noty:hover'];
events.forEach(function(eventName) {
window.addEventListener(eventName, function(event) {
if (typeof Livewire === 'undefined') {
return;
}
const { detail } = event;
const { envelope } = detail;
const context = envelope.context || {};
if (!context.livewire?.id) {
return;
}
const { livewire: { id: componentId } } = context;
const component = Livewire.all().find(c => c.id === componentId);
if (!component) {
return;
}
const livewireEventName = eventName.replace('flasher:', '').replace(':', ':');
Livewire.dispatchTo(component.name, livewireEventName, { payload: detail });
}, false);
});
})();
</script>
JAVASCRIPT;
$event->setResponse($response);
}
public function getSubscribedEvents(): string
{
return ResponseEvent::class;
}
}
+34
View File
@@ -29,6 +29,34 @@ export default class NotyPlugin extends AbstractPlugin {
Object.assign(options, envelope.options)
}
// Wrap callbacks to dispatch events
const originalCallbacks = {
onShow: options.callbacks?.onShow,
onClick: options.callbacks?.onClick,
onClose: options.callbacks?.onClose,
onHover: options.callbacks?.onHover,
}
options.callbacks = {
...options.callbacks,
onShow: () => {
this.dispatchEvent('flasher:noty:show', envelope)
originalCallbacks.onShow?.()
},
onClick: () => {
this.dispatchEvent('flasher:noty:click', envelope)
originalCallbacks.onClick?.()
},
onClose: () => {
this.dispatchEvent('flasher:noty:close', envelope)
originalCallbacks.onClose?.()
},
onHover: () => {
this.dispatchEvent('flasher:noty:hover', envelope)
originalCallbacks.onHover?.()
},
}
const noty = new Noty(options)
noty.show()
@@ -42,6 +70,12 @@ export default class NotyPlugin extends AbstractPlugin {
})
}
private dispatchEvent(eventName: string, envelope: Envelope): void {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}))
}
public renderOptions(options: Options): void {
if (!options) {
return
+29
View File
@@ -96,11 +96,35 @@ class NotyPlugin extends AbstractPlugin {
return;
}
envelopes.forEach((envelope) => {
var _a, _b, _c, _d;
try {
const options = Object.assign({ text: envelope.message, type: envelope.type }, this.defaultOptions);
if (envelope.options) {
Object.assign(options, envelope.options);
}
const originalCallbacks = {
onShow: (_a = options.callbacks) === null || _a === void 0 ? void 0 : _a.onShow,
onClick: (_b = options.callbacks) === null || _b === void 0 ? void 0 : _b.onClick,
onClose: (_c = options.callbacks) === null || _c === void 0 ? void 0 : _c.onClose,
onHover: (_d = options.callbacks) === null || _d === void 0 ? void 0 : _d.onHover,
};
options.callbacks = Object.assign(Object.assign({}, options.callbacks), { onShow: () => {
var _a;
this.dispatchEvent('flasher:noty:show', envelope);
(_a = originalCallbacks.onShow) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClick: () => {
var _a;
this.dispatchEvent('flasher:noty:click', envelope);
(_a = originalCallbacks.onClick) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClose: () => {
var _a;
this.dispatchEvent('flasher:noty:close', envelope);
(_a = originalCallbacks.onClose) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onHover: () => {
var _a;
this.dispatchEvent('flasher:noty:hover', envelope);
(_a = originalCallbacks.onHover) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
} });
const noty = new Noty(options);
noty.show();
const layoutDom = noty.layoutDom;
@@ -113,6 +137,11 @@ class NotyPlugin extends AbstractPlugin {
}
});
}
dispatchEvent(eventName, envelope) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}));
}
renderOptions(options) {
if (!options) {
return;
+29
View File
@@ -99,11 +99,35 @@
return;
}
envelopes.forEach((envelope) => {
var _a, _b, _c, _d;
try {
const options = Object.assign({ text: envelope.message, type: envelope.type }, this.defaultOptions);
if (envelope.options) {
Object.assign(options, envelope.options);
}
const originalCallbacks = {
onShow: (_a = options.callbacks) === null || _a === void 0 ? void 0 : _a.onShow,
onClick: (_b = options.callbacks) === null || _b === void 0 ? void 0 : _b.onClick,
onClose: (_c = options.callbacks) === null || _c === void 0 ? void 0 : _c.onClose,
onHover: (_d = options.callbacks) === null || _d === void 0 ? void 0 : _d.onHover,
};
options.callbacks = Object.assign(Object.assign({}, options.callbacks), { onShow: () => {
var _a;
this.dispatchEvent('flasher:noty:show', envelope);
(_a = originalCallbacks.onShow) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClick: () => {
var _a;
this.dispatchEvent('flasher:noty:click', envelope);
(_a = originalCallbacks.onClick) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onClose: () => {
var _a;
this.dispatchEvent('flasher:noty:close', envelope);
(_a = originalCallbacks.onClose) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
}, onHover: () => {
var _a;
this.dispatchEvent('flasher:noty:hover', envelope);
(_a = originalCallbacks.onHover) === null || _a === void 0 ? void 0 : _a.call(originalCallbacks);
} });
const noty = new Noty(options);
noty.show();
const layoutDom = noty.layoutDom;
@@ -116,6 +140,11 @@
}
});
}
dispatchEvent(eventName, envelope) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope },
}));
}
renderOptions(options) {
if (!options) {
return;
+1 -1
View File
@@ -1 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,o){let i,n,r,l={};if("object"==typeof e?(l=Object.assign({},e),i=l.type,n=l.message,r=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),i=e,n=l.message,r=l.title,delete l.message,delete l.title):(i=e,n=t,null==s?(r=void 0,l=o||{}):"string"==typeof s?(r=s,l=o||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(r=l.title,delete l.title):r=void 0,o&&"object"==typeof o&&(l=Object.assign(Object.assign({},l),o)))),!i)throw new Error("Type is required for notifications");if(null==n)throw new Error("Message is required for notifications");null==r&&(r=i.charAt(0).toUpperCase()+i.slice(1));const a={type:i,message:n,title:r,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}const o=new class extends s{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{try{const s=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(s,e.options);const o=new t(s);o.show();const i=o.layoutDom;i&&"object"==typeof i.dataset&&(i.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",o),o}));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class o{success(e,t,o){this.flash("success",e,t,o)}error(e,t,o){this.flash("error",e,t,o)}info(e,t,o){this.flash("info",e,t,o)}warning(e,t,o){this.flash("warning",e,t,o)}flash(e,t,o,s){let n,i,l,a={};if("object"==typeof e?(a=Object.assign({},e),n=a.type,i=a.message,l=a.title,delete a.type,delete a.message,delete a.title):"object"==typeof t?(a=Object.assign({},t),n=e,i=a.message,l=a.title,delete a.message,delete a.title):(n=e,i=t,null==o?(l=void 0,a=s||{}):"string"==typeof o?(l=o,a=s||{}):"object"==typeof o&&(a=Object.assign({},o),"title"in a?(l=a.title,delete a.title):l=void 0,s&&"object"==typeof s&&(a=Object.assign(Object.assign({},a),s)))),!n)throw new Error("Type is required for notifications");if(null==i)throw new Error("Message is required for notifications");null==l&&(l=n.charAt(0).toUpperCase()+n.slice(1));const r={type:n,message:i,title:l,options:a,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([r])}}const s=new class extends o{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{var o,s,n,i;try{const l=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(l,e.options);const a={onShow:null===(o=l.callbacks)||void 0===o?void 0:o.onShow,onClick:null===(s=l.callbacks)||void 0===s?void 0:s.onClick,onClose:null===(n=l.callbacks)||void 0===n?void 0:n.onClose,onHover:null===(i=l.callbacks)||void 0===i?void 0:i.onHover};l.callbacks=Object.assign(Object.assign({},l.callbacks),{onShow:()=>{var t;this.dispatchEvent("flasher:noty:show",e),null===(t=a.onShow)||void 0===t||t.call(a)},onClick:()=>{var t;this.dispatchEvent("flasher:noty:click",e),null===(t=a.onClick)||void 0===t||t.call(a)},onClose:()=>{var t;this.dispatchEvent("flasher:noty:close",e),null===(t=a.onClose)||void 0===t||t.call(a)},onHover:()=>{var t;this.dispatchEvent("flasher:noty:hover",e),null===(t=a.onHover)||void 0===t||t.call(a)}});const r=new t(l);r.show();const c=r.layoutDom;c&&"object"==typeof c.dataset&&(c.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}dispatchEvent(e,t){window.dispatchEvent(new CustomEvent(e,{detail:{envelope:t}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",s),s}));
+1
View File
@@ -3,5 +3,6 @@ import type { Envelope, Options } from '@flasher/flasher/dist/types';
export default class NotyPlugin extends AbstractPlugin {
private defaultOptions;
renderEnvelopes(envelopes: Envelope[]): void;
private dispatchEvent;
renderOptions(options: Options): void;
}
+1 -1
View File
@@ -1 +1 @@
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class s{success(e,t,s){this.flash("success",e,t,s)}error(e,t,s){this.flash("error",e,t,s)}info(e,t,s){this.flash("info",e,t,s)}warning(e,t,s){this.flash("warning",e,t,s)}flash(e,t,s,o){let i,n,r,l={};if("object"==typeof e?(l=Object.assign({},e),i=l.type,n=l.message,r=l.title,delete l.type,delete l.message,delete l.title):"object"==typeof t?(l=Object.assign({},t),i=e,n=l.message,r=l.title,delete l.message,delete l.title):(i=e,n=t,null==s?(r=void 0,l=o||{}):"string"==typeof s?(r=s,l=o||{}):"object"==typeof s&&(l=Object.assign({},s),"title"in l?(r=l.title,delete l.title):r=void 0,o&&"object"==typeof o&&(l=Object.assign(Object.assign({},l),o)))),!i)throw new Error("Type is required for notifications");if(null==n)throw new Error("Message is required for notifications");null==r&&(r=i.charAt(0).toUpperCase()+i.slice(1));const a={type:i,message:n,title:r,options:l,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([a])}}const o=new class extends s{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{try{const s=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(s,e.options);const o=new t(s);o.show();const i=o.layoutDom;i&&"object"==typeof i.dataset&&(i.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",o),o}));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("@flasher/flasher"),require("noty")):"function"==typeof define&&define.amd?define(["@flasher/flasher","noty"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).Noty=t(e.flasher,e.Noty)}(this,(function(e,t){"use strict";class o{success(e,t,o){this.flash("success",e,t,o)}error(e,t,o){this.flash("error",e,t,o)}info(e,t,o){this.flash("info",e,t,o)}warning(e,t,o){this.flash("warning",e,t,o)}flash(e,t,o,s){let n,i,l,a={};if("object"==typeof e?(a=Object.assign({},e),n=a.type,i=a.message,l=a.title,delete a.type,delete a.message,delete a.title):"object"==typeof t?(a=Object.assign({},t),n=e,i=a.message,l=a.title,delete a.message,delete a.title):(n=e,i=t,null==o?(l=void 0,a=s||{}):"string"==typeof o?(l=o,a=s||{}):"object"==typeof o&&(a=Object.assign({},o),"title"in a?(l=a.title,delete a.title):l=void 0,s&&"object"==typeof s&&(a=Object.assign(Object.assign({},a),s)))),!n)throw new Error("Type is required for notifications");if(null==i)throw new Error("Message is required for notifications");null==l&&(l=n.charAt(0).toUpperCase()+n.slice(1));const r={type:n,message:i,title:l,options:a,metadata:{plugin:""}};this.renderOptions({}),this.renderEnvelopes([r])}}const s=new class extends o{constructor(){super(...arguments),this.defaultOptions={timeout:1e4}}renderEnvelopes(e){(null==e?void 0:e.length)&&e.forEach((e=>{var o,s,n,i;try{const l=Object.assign({text:e.message,type:e.type},this.defaultOptions);e.options&&Object.assign(l,e.options);const a={onShow:null===(o=l.callbacks)||void 0===o?void 0:o.onShow,onClick:null===(s=l.callbacks)||void 0===s?void 0:s.onClick,onClose:null===(n=l.callbacks)||void 0===n?void 0:n.onClose,onHover:null===(i=l.callbacks)||void 0===i?void 0:i.onHover};l.callbacks=Object.assign(Object.assign({},l.callbacks),{onShow:()=>{var t;this.dispatchEvent("flasher:noty:show",e),null===(t=a.onShow)||void 0===t||t.call(a)},onClick:()=>{var t;this.dispatchEvent("flasher:noty:click",e),null===(t=a.onClick)||void 0===t||t.call(a)},onClose:()=>{var t;this.dispatchEvent("flasher:noty:close",e),null===(t=a.onClose)||void 0===t||t.call(a)},onHover:()=>{var t;this.dispatchEvent("flasher:noty:hover",e),null===(t=a.onHover)||void 0===t||t.call(a)}});const r=new t(l);r.show();const c=r.layoutDom;c&&"object"==typeof c.dataset&&(c.dataset.turboTemporary="")}catch(t){console.error("PHPFlasher Noty: Error rendering notification",t,e)}}))}dispatchEvent(e,t){window.dispatchEvent(new CustomEvent(e,{detail:{envelope:t}}))}renderOptions(e){e&&(Object.assign(this.defaultOptions,e),t.overrideDefaults(this.defaultOptions))}};return e.addPlugin("noty",s),s}));
+119 -10
View File
@@ -1,34 +1,143 @@
# PHPFlasher Noty Adapter
[![Latest Stable Version](https://img.shields.io/packagist/v/php-flasher/flasher-noty.svg)](https://packagist.org/packages/php-flasher/flasher-noty)
[![Latest Version](https://img.shields.io/packagist/v/php-flasher/flasher-noty.svg)](https://packagist.org/packages/php-flasher/flasher-noty)
[![Total Downloads](https://img.shields.io/packagist/dt/php-flasher/flasher-noty.svg)](https://packagist.org/packages/php-flasher/flasher-noty)
[![License](https://img.shields.io/packagist/l/php-flasher/flasher-noty.svg)](https://packagist.org/packages/php-flasher/flasher-noty)
Noty adapter for [PHPFlasher](https://php-flasher.io).
Dependency-free notification library using [Noty](https://ned.im/noty/) for PHPFlasher.
## Features
- Multiple layout positions (top, bottom, center, corners)
- Queue management for multiple notifications
- Configurable animations
- Action buttons support
- Progress bar for timeout
- Modal mode support
## Installation
**Laravel:**
```bash
composer require php-flasher/flasher-noty-laravel # Laravel
composer require php-flasher/flasher-noty-symfony # Symfony
composer require php-flasher/flasher-noty-laravel
php artisan flasher:install
```
**Symfony:**
```bash
composer require php-flasher/flasher-noty-symfony
php bin/console flasher:install
```
## Quick Start
```php
// Basic usage
flash('noty')->success('Operation completed successfully!');
// Using the helper function
noty()->success('Profile updated successfully!');
// With options
flash('noty')->warning('Please backup your data.', [
'timeout' => 3000,
// With custom layout
noty()->info('New message received', [
'layout' => 'topCenter',
]);
// Error notification
noty()->error('Unable to save changes. Please try again.');
// Warning with custom timeout
noty()->warning('Session expires in 5 minutes', [
'timeout' => 10000,
]);
// Sticky notification (no auto-dismiss)
noty()->error('Connection lost', ['timeout' => false]);
// With progress bar
noty()->info('Processing...', ['progressBar' => true]);
```
## Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `layout` | string | `topRight` | Notification position |
| `timeout` | int/bool | `5000` | Auto-dismiss delay in ms (`false` = sticky) |
| `progressBar` | bool | `true` | Show countdown progress bar |
| `closeWith` | array | `['click']` | How to close (`click`, `button`) |
| `animation.open` | string | `null` | Opening animation class |
| `animation.close` | string | `null` | Closing animation class |
| `modal` | bool | `false` | Show as modal with overlay |
| `killer` | bool | `false` | Close all other notifications |
### Layout Options
- `top`, `topLeft`, `topCenter`, `topRight`
- `center`, `centerLeft`, `centerRight`
- `bottom`, `bottomLeft`, `bottomCenter`, `bottomRight`
## Livewire Integration
Noty notifications work seamlessly with Livewire. You can listen to notification events:
```php
use Livewire\Attributes\On;
#[On('noty:click')]
public function onNotyClick(array $payload): void
{
// Handle notification click
}
#[On('noty:close')]
public function onNotyClose(array $payload): void
{
// Handle notification close
}
```
### Available Events
| Event | Description |
|-------|-------------|
| `noty:show` | Fired when notification is shown |
| `noty:click` | Fired when notification is clicked |
| `noty:close` | Fired when close button is clicked |
| `noty:hidden` | Fired when notification is hidden |
## Global Configuration
**Laravel** (`config/flasher.php`):
```php
'plugins' => [
'noty' => [
'options' => [
'layout' => 'topRight',
'timeout' => 5000,
'progressBar' => true,
'closeWith' => ['click', 'button'],
],
],
],
```
**Symfony** (`config/packages/flasher.yaml`):
```yaml
flasher:
plugins:
noty:
options:
layout: topRight
timeout: 5000
progressBar: true
closeWith: ['click', 'button']
```
## Documentation
For complete documentation, visit [php-flasher.io](https://php-flasher.io).
For complete documentation, visit [php-flasher.io/library/noty](https://php-flasher.io/library/noty).
## License
@@ -6,6 +6,7 @@ namespace Flasher\Notyf\Laravel;
use Flasher\Laravel\Support\PluginServiceProvider;
use Flasher\Notyf\Prime\NotyfPlugin;
use Flasher\Prime\EventDispatcher\EventDispatcherInterface;
final class FlasherNotyfServiceProvider extends PluginServiceProvider
{
@@ -13,4 +14,22 @@ final class FlasherNotyfServiceProvider extends PluginServiceProvider
{
return new NotyfPlugin();
}
protected function afterBoot(): void
{
$this->registerLivewireListener();
}
private function registerLivewireListener(): void
{
if (!$this->app->bound('livewire')) {
return;
}
$this->app->extend('flasher.event_dispatcher', static function (EventDispatcherInterface $dispatcher) {
$dispatcher->addListener(new LivewireListener());
return $dispatcher;
});
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Flasher\Notyf\Laravel;
use Flasher\Prime\EventDispatcher\Event\ResponseEvent;
use Flasher\Prime\EventDispatcher\EventListener\EventListenerInterface;
final readonly class LivewireListener implements EventListenerInterface
{
public function __invoke(ResponseEvent $event): void
{
// Only process HTML responses
if ('html' !== $event->getPresenter()) {
return;
}
$response = $event->getResponse() ?: '';
if (!\is_string($response)) {
return;
}
// Avoid duplicate script injection
if (false === strripos($response, '<script type="text/javascript" class="flasher-js"')) {
return;
}
if (strripos($response, '<script type="text/javascript" class="flasher-notyf-livewire-js"')) {
return;
}
// Inject the Notyf-Livewire bridge JavaScript
$response .= <<<'JAVASCRIPT'
<script type="text/javascript" class="flasher-notyf-livewire-js">
(function() {
const events = ['flasher:notyf:click', 'flasher:notyf:dismiss'];
events.forEach(function(eventName) {
window.addEventListener(eventName, function(event) {
if (typeof Livewire === 'undefined') {
return;
}
const { detail } = event;
const { envelope } = detail;
const context = envelope.context || {};
if (!context.livewire?.id) {
return;
}
const { livewire: { id: componentId } } = context;
const component = Livewire.all().find(c => c.id === componentId);
if (!component) {
return;
}
const livewireEventName = eventName.replace('flasher:', '').replace(':', ':');
Livewire.dispatchTo(component.name, livewireEventName, { payload: detail });
}, false);
});
})();
</script>
JAVASCRIPT;
$event->setResponse($response);
}
public function getSubscribedEvents(): string
{
return ResponseEvent::class;
}
}
+36 -1
View File
@@ -16,7 +16,11 @@ export default class NotyfPlugin extends AbstractPlugin {
envelopes.forEach((envelope) => {
try {
const options = { ...envelope, ...envelope.options }
this.notyf?.open(options)
const notification = this.notyf?.open(options)
if (notification) {
this.attachEventListeners(notification, envelope)
}
} catch (error) {
console.error('PHPFlasher Notyf: Error rendering notification', error, envelope)
}
@@ -92,4 +96,35 @@ export default class NotyfPlugin extends AbstractPlugin {
types.push(newType)
}
}
private attachEventListeners(notification: unknown, envelope: Envelope): void {
if (!this.notyf) {
return
}
// Notyf supports events at runtime but types don't include them
const notyf = this.notyf as unknown as {
on: (event: string, callback: (params: { target: unknown, event: Event }) => void) => void
}
// Listen for click events
notyf.on('click', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:click', envelope, { event })
}
})
// Listen for dismiss events
notyf.on('dismiss', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:dismiss', envelope, { event })
}
})
}
private dispatchEvent(eventName: string, envelope: Envelope, extra: Record<string, any> = {}): void {
window.dispatchEvent(new CustomEvent(eventName, {
detail: { envelope, ...extra },
}))
}
}
+25 -1
View File
@@ -515,7 +515,10 @@ class NotyfPlugin extends AbstractPlugin {
var _a;
try {
const options = Object.assign(Object.assign({}, envelope), envelope.options);
(_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
const notification = (_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
if (notification) {
this.attachEventListeners(notification, envelope);
}
}
catch (error) {
console.error('PHPFlasher Notyf: Error rendering notification', error, envelope);
@@ -579,6 +582,27 @@ class NotyfPlugin extends AbstractPlugin {
types.push(newType);
}
}
attachEventListeners(notification, envelope) {
if (!this.notyf) {
return;
}
const notyf = this.notyf;
notyf.on('click', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:click', envelope, { event });
}
});
notyf.on('dismiss', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:dismiss', envelope, { event });
}
});
}
dispatchEvent(eventName, envelope, extra = {}) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: Object.assign({ envelope }, extra),
}));
}
}
const notyf = new NotyfPlugin();
+25 -1
View File
@@ -519,7 +519,10 @@
var _a;
try {
const options = Object.assign(Object.assign({}, envelope), envelope.options);
(_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
const notification = (_a = this.notyf) === null || _a === void 0 ? void 0 : _a.open(options);
if (notification) {
this.attachEventListeners(notification, envelope);
}
}
catch (error) {
console.error('PHPFlasher Notyf: Error rendering notification', error, envelope);
@@ -583,6 +586,27 @@
types.push(newType);
}
}
attachEventListeners(notification, envelope) {
if (!this.notyf) {
return;
}
const notyf = this.notyf;
notyf.on('click', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:click', envelope, { event });
}
});
notyf.on('dismiss', ({ target, event }) => {
if (target === notification) {
this.dispatchEvent('flasher:notyf:dismiss', envelope, { event });
}
});
}
dispatchEvent(eventName, envelope, extra = {}) {
window.dispatchEvent(new CustomEvent(eventName, {
detail: Object.assign({ envelope }, extra),
}));
}
}
const notyf = new NotyfPlugin();
File diff suppressed because one or more lines are too long
+2
View File
@@ -7,4 +7,6 @@ export default class NotyfPlugin extends AbstractPlugin {
renderOptions(options: Options): void;
private initializeNotyf;
private addTypeIfNotExists;
private attachEventListeners;
private dispatchEvent;
}
File diff suppressed because one or more lines are too long
+125 -11
View File
@@ -1,34 +1,148 @@
# PHPFlasher Notyf Adapter
[![Latest Stable Version](https://img.shields.io/packagist/v/php-flasher/flasher-notyf.svg)](https://packagist.org/packages/php-flasher/flasher-notyf)
[![Latest Version](https://img.shields.io/packagist/v/php-flasher/flasher-notyf.svg)](https://packagist.org/packages/php-flasher/flasher-notyf)
[![Total Downloads](https://img.shields.io/packagist/dt/php-flasher/flasher-notyf.svg)](https://packagist.org/packages/php-flasher/flasher-notyf)
[![License](https://img.shields.io/packagist/l/php-flasher/flasher-notyf.svg)](https://packagist.org/packages/php-flasher/flasher-notyf)
Notyf adapter for [PHPFlasher](https://php-flasher.io).
Minimalist, responsive toast notifications using [Notyf](https://github.com/caroso1222/notyf) for PHPFlasher.
## Features
- Lightweight and minimalist design
- Responsive and mobile-friendly
- Customizable positions
- Dismissible notifications
- Ripple effect animation
- Custom icons support
## Installation
**Laravel:**
```bash
composer require php-flasher/flasher-notyf-laravel # Laravel
composer require php-flasher/flasher-notyf-symfony # Symfony
composer require php-flasher/flasher-notyf-laravel
php artisan flasher:install
```
**Symfony:**
```bash
composer require php-flasher/flasher-notyf-symfony
php bin/console flasher:install
```
## Quick Start
```php
// Basic usage
flash('notyf')->success('Operation completed successfully!');
// Using the helper function
notyf()->success('Profile updated successfully!');
// With options
flash('notyf')->info('New message received', [
'duration' => 4000,
'position' => 'top-right',
// With custom position
notyf()->info('New notification', [
'position' => ['x' => 'center', 'y' => 'top'],
]);
// Error notification
notyf()->error('Unable to save changes.');
// Custom duration
notyf()->warning('Please review your input', [
'duration' => 8000,
]);
// Dismissible notification
notyf()->success('Click to dismiss', [
'dismissible' => true,
]);
// With ripple effect
notyf()->info('Loading...', ['ripple' => true]);
```
## Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `duration` | int | `5000` | Auto-dismiss delay in ms (0 = sticky) |
| `dismissible` | bool | `false` | Show close button |
| `ripple` | bool | `true` | Enable ripple animation |
| `position.x` | string | `right` | Horizontal position (`left`, `center`, `right`) |
| `position.y` | string | `top` | Vertical position (`top`, `bottom`) |
### Position Combinations
- `{x: 'right', y: 'top'}` (default)
- `{x: 'left', y: 'top'}`
- `{x: 'center', y: 'top'}`
- `{x: 'right', y: 'bottom'}`
- `{x: 'left', y: 'bottom'}`
- `{x: 'center', y: 'bottom'}`
## Livewire Integration
Notyf notifications work seamlessly with Livewire. You can listen to notification events:
```php
use Livewire\Attributes\On;
#[On('notyf:click')]
public function onNotyfClick(array $payload): void
{
// Handle notification click
}
#[On('notyf:dismiss')]
public function onNotyfDismiss(array $payload): void
{
// Handle notification dismiss
}
```
### Available Events
| Event | Description |
|-------|-------------|
| `notyf:click` | Fired when notification is clicked |
| `notyf:dismiss` | Fired when notification is dismissed |
## Global Configuration
**Laravel** (`config/flasher.php`):
```php
'plugins' => [
'notyf' => [
'options' => [
'duration' => 5000,
'dismissible' => true,
'ripple' => true,
'position' => [
'x' => 'right',
'y' => 'top',
],
],
],
],
```
**Symfony** (`config/packages/flasher.yaml`):
```yaml
flasher:
plugins:
notyf:
options:
duration: 5000
dismissible: true
ripple: true
position:
x: right
y: top
```
## Documentation
For complete documentation, visit [php-flasher.io](https://php-flasher.io).
For complete documentation, visit [php-flasher.io/library/notyf](https://php-flasher.io/library/notyf).
## License
+74 -7
View File
@@ -78,7 +78,11 @@ export default class FlasherPlugin extends AbstractPlugin {
// Wait for DOM to be ready if needed
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', render)
const onDOMReady = () => {
document.removeEventListener('DOMContentLoaded', onDOMReady)
render()
}
document.addEventListener('DOMContentLoaded', onDOMReady)
} else {
render()
}
@@ -104,7 +108,9 @@ export default class FlasherPlugin extends AbstractPlugin {
// Apply custom styles
Object.entries(options.style).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
container.style.setProperty(key, String(value))
// Convert camelCase to kebab-case for CSS property names
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
container.style.setProperty(cssKey, String(value))
}
})
@@ -162,6 +168,15 @@ export default class FlasherPlugin extends AbstractPlugin {
})
}
// Add click event listener to dispatch theme events
notification.addEventListener('click', (event) => {
// Don't trigger if clicking the close button
if ((event.target as HTMLElement).closest('.fl-close')) {
return
}
this.dispatchClickEvents(envelope)
})
// Add timer if timeout is greater than 0 (not sticky)
if (options.timeout > 0) {
this.addTimer(notification, options)
@@ -233,12 +248,23 @@ export default class FlasherPlugin extends AbstractPlugin {
// Start timer
intervalId = window.setInterval(updateTimer, lapse)
// Pause timer on hover
notification.addEventListener('mouseout', () => {
// Define event handlers so we can remove them later
const handleMouseOut = () => {
clearInterval(intervalId)
intervalId = window.setInterval(updateTimer, lapse)
})
notification.addEventListener('mouseover', () => clearInterval(intervalId))
}
const handleMouseOver = () => clearInterval(intervalId)
// Pause timer on hover
notification.addEventListener('mouseout', handleMouseOut)
notification.addEventListener('mouseover', handleMouseOver)
// Store cleanup function on the element for later removal
;(notification as any)._flasherCleanup = () => {
clearInterval(intervalId)
notification.removeEventListener('mouseout', handleMouseOut)
notification.removeEventListener('mouseover', handleMouseOver)
}
}
private removeNotification(notification: HTMLElement): void {
@@ -246,6 +272,12 @@ export default class FlasherPlugin extends AbstractPlugin {
return
}
// Clean up event listeners and timers to prevent memory leaks
if ((notification as any)._flasherCleanup) {
(notification as any)._flasherCleanup()
delete (notification as any)._flasherCleanup
}
notification.classList.remove('fl-show')
// Clean up empty containers after animation
@@ -262,7 +294,13 @@ export default class FlasherPlugin extends AbstractPlugin {
private stringToHTML(str: string): HTMLElement {
const template = document.createElement('template')
template.innerHTML = str.trim()
return template.content.firstElementChild as HTMLElement
const element = template.content.firstElementChild
if (!element) {
throw new Error('PHPFlasher: Invalid HTML template - no element found')
}
return element as HTMLElement
}
private escapeHtml(str: string | null | undefined): string {
@@ -283,4 +321,33 @@ export default class FlasherPlugin extends AbstractPlugin {
return str.replace(/[&<>"'`=/]/g, (char) => htmlEscapes[char] || char)
}
private dispatchClickEvents(envelope: Envelope): void {
const detail = { envelope }
// Dispatch generic theme click event
window.dispatchEvent(new CustomEvent('flasher:theme:click', { detail }))
// Dispatch theme-specific click event (e.g., flasher:theme:flasher:click)
const themeName = this.getThemeName(envelope)
if (themeName) {
window.dispatchEvent(new CustomEvent(`flasher:theme:${themeName}:click`, { detail }))
}
}
private getThemeName(envelope: Envelope): string {
const plugin = envelope.metadata?.plugin || ''
// Extract theme name from plugin (e.g., 'theme.flasher' -> 'flasher')
if (plugin.startsWith('theme.')) {
return plugin.replace('theme.', '')
}
// If it's the default 'flasher' plugin, return 'flasher'
if (plugin === 'flasher') {
return 'flasher'
}
return plugin
}
}
+25 -9
View File
@@ -128,7 +128,7 @@ export default class Flasher extends AbstractPlugin {
envelope.metadata.plugin = this.resolvePluginAlias(envelope.metadata.plugin)
this.addThemeStyles(resolved, envelope.metadata.plugin)
envelope.options = this.resolveOptions(envelope.options)
envelope.context = response.context as Context
envelope.context = resolved.context
})
return resolved
@@ -156,17 +156,32 @@ export default class Flasher extends AbstractPlugin {
const functionRegex = /^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/
const arrowFunctionRegex = /^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/
const match = func.match(functionRegex) || func.match(arrowFunctionRegex)
if (!match) {
const functionMatch = func.match(functionRegex)
const arrowMatch = func.match(arrowFunctionRegex)
if (!functionMatch && !arrowMatch) {
return func
}
const args = match[2]?.split(',').map((arg) => arg.trim()) ?? []
let body = match[3].trim()
let args: string[]
let body: string
// Arrow functions with a single expression can omit the curly braces and the return keyword
if (!body.startsWith('{')) {
body = `{ return ${body}; }`
if (functionMatch) {
// Regular function: body is already complete statements
args = functionMatch[2]?.split(',').map((arg) => arg.trim()).filter(Boolean) ?? []
body = functionMatch[3].trim()
} else {
// Arrow function: may need to wrap expression body with return
args = arrowMatch![2]?.split(',').map((arg) => arg.trim()).filter(Boolean) ?? []
body = arrowMatch![3].trim()
// Arrow functions with a single expression need return added
if (!body.startsWith('{')) {
body = `return ${body};`
} else {
// Remove outer braces for arrow functions with block body
body = body.slice(1, -1).trim()
}
}
try {
@@ -249,7 +264,8 @@ export default class Flasher extends AbstractPlugin {
private loadAsset(url: string, nonce: string, type: 'style' | 'script'): Promise<void> {
// Check if asset is already loaded
if (document.querySelector(`${type === 'style' ? 'link' : 'script'}[src="${url}"]`)) {
const selector = type === 'style' ? `link[href="${url}"]` : `script[src="${url}"]`
if (document.querySelector(selector)) {
return Promise.resolve()
}
@@ -1,63 +1,37 @@
import './amazon.scss'
import type { Envelope } from '../../types'
import { getTypeIcon, getCloseIcon } from '../shared/icons'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES, DEFAULT_TITLES } from '../shared/constants'
const AMAZON_TITLES: Record<string, string> = {
success: 'Success!',
error: 'Problem',
warning: 'Warning',
info: 'Information',
}
export const amazonTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
const getAlertIcon = () => {
switch (type) {
case 'success':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>`
case 'error':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>`
case 'warning':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>`
case 'info':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>`
}
return ''
}
const getAlertTitle = () => {
switch (type) {
case 'success': return 'Success!'
case 'error': return 'Problem'
case 'warning': return 'Warning'
case 'info': return 'Information'
default: return 'Alert'
}
}
const alertTitle = AMAZON_TITLES[type] || DEFAULT_TITLES[type] || 'Alert'
return `
<div class="fl-amazon fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('amazon')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-amazon-alert">
<div class="fl-alert-content">
<div class="fl-icon-container">
${getAlertIcon()}
${getTypeIcon(type, { size: 'lg' })}
</div>
<div class="fl-text-content">
<div class="fl-alert-title">${getAlertTitle()}</div>
<div class="fl-alert-title">${alertTitle}</div>
<div class="fl-alert-message">${message}</div>
</div>
</div>
<div class="fl-alert-actions">
<button class="fl-close" aria-label="Close notification">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
${getCloseIcon()}
</button>
</div>
</div>
@@ -1,25 +1,23 @@
import './amber.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const amberTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-amber fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-icon"></div>
<div class="fl-text">
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.theme('amber')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.icon}"></div>
<div class="${CLASS_NAMES.text}">
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
@@ -1,22 +1,20 @@
import './aurora.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const auroraTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-aurora fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-message">${message}</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<div class="${CLASS_NAMES.theme('aurora')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
@@ -1,24 +1,22 @@
import './crystal.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const crystalTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-crystal fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-text">
<p class="fl-message">${message}</p>
<div class="${CLASS_NAMES.theme('crystal')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.text}">
<p class="${CLASS_NAMES.message}">${message}</p>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
@@ -1,19 +1,17 @@
import './emerald.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const emeraldTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-emerald fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-message">${message}</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<div class="${CLASS_NAMES.theme('emerald')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
</div>`
},
@@ -1,17 +1,18 @@
import './facebook.scss'
import type { Envelope } from '../../types'
import { getCloseIcon } from '../shared/icons'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
function getTimeString(): string {
const now = new Date()
return now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
}
export const facebookTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
const now = new Date()
const timeString = now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
const getNotificationIcon = () => {
switch (type) {
case 'success':
@@ -43,25 +44,23 @@ export const facebookTheme = {
}
return `
<div class="fl-facebook fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('facebook')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-fb-notification">
<div class="fl-icon-container">
${getNotificationIcon()}
</div>
<div class="fl-content">
<div class="fl-message">
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">
${message}
</div>
<div class="fl-meta">
<span class="fl-time">${timeString}</span>
<span class="fl-time">${getTimeString()}</span>
</div>
</div>
<div class="fl-actions">
<button class="fl-button fl-close" aria-label="Close ${type} message">
<div class="${CLASS_NAMES.actions}">
<button class="fl-button ${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
<div class="fl-button-icon">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
${getCloseIcon({ size: 'md' })}
</div>
</button>
</div>
@@ -1,28 +1,26 @@
import './flasher.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES, getTitle } from '../shared/constants'
export const flasherTheme = {
render: (envelope: Envelope): string => {
const { type, title, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
const displayTitle = title || type.charAt(0).toUpperCase() + type.slice(1)
const displayTitle = getTitle(title, type)
return `
<div class="fl-flasher fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-icon"></div>
<div class="${CLASS_NAMES.theme('flasher')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.icon}"></div>
<div>
<strong class="fl-title">${displayTitle}</strong>
<span class="fl-message">${message}</span>
<strong class="${CLASS_NAMES.title}">${displayTitle}</strong>
<span class="${CLASS_NAMES.message}">${message}</span>
</div>
<button class="fl-close" aria-label="Close ${type} message">&times;</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>&times;</button>
</div>
<span class="fl-progress-bar">
<span class="fl-progress"></span>
<span class="${CLASS_NAMES.progressBar}">
<span class="${CLASS_NAMES.progress}"></span>
</span>
</div>`
},
@@ -1,60 +1,35 @@
import './google.scss'
import type { Envelope } from '../../types'
import { getTypeIcon } from '../shared/icons'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES, DEFAULT_TEXT } from '../shared/constants'
export const googleTheme = {
render: (envelope: Envelope): string => {
const { type, message, title } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
const actionText = 'DISMISS'
const getIcon = () => {
switch (type) {
case 'success':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>`
case 'error':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>
</svg>`
case 'warning':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z"/>
</svg>`
case 'info':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>`
}
return ''
}
const titleSection = title ? `<div class="fl-title">${title}</div>` : ''
const titleSection = title ? `<div class="${CLASS_NAMES.title}">${title}</div>` : ''
return `
<div class="fl-google fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('google')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-md-card">
<div class="fl-content">
<div class="fl-icon-wrapper">
${getIcon()}
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.iconWrapper}">
${getTypeIcon(type, { size: 'lg', className: 'fl-icon-svg' })}
</div>
<div class="fl-text-content">
${titleSection}
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
</div>
<div class="fl-actions">
<button class="fl-action-button fl-close" aria-label="Close ${type} message">
${actionText}
<div class="${CLASS_NAMES.actions}">
<button class="fl-action-button ${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
${DEFAULT_TEXT.dismissButton}
</button>
</div>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
+46 -1
View File
@@ -2,6 +2,7 @@
@use "sass:meta";
:root {
// Colors
--fl-success: #10b981;
--fl-info: #3b82f6;
--fl-warning: #f59e0b;
@@ -19,12 +20,56 @@
--fl-text-light: rgb(75, 85, 99);
--fl-text-dark: var(--fl-white);
// Typography
--fl-font: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--fl-font-size-xs: 0.75rem;
--fl-font-size-sm: 0.875rem;
--fl-font-size-base: 1rem;
--fl-font-size-lg: 1.125rem;
--fl-font-size-xl: 1.25rem;
// Spacing scale (rem for consistency)
--fl-spacing-xs: 0.25rem;
--fl-spacing-sm: 0.5rem;
--fl-spacing-md: 0.75rem;
--fl-spacing-lg: 1rem;
--fl-spacing-xl: 1.5rem;
--fl-spacing-2xl: 2rem;
// Close button sizes
--fl-close-sm: 1.125rem;
--fl-close-md: 1.5rem;
--fl-close-lg: 1.875rem;
// Border radius
--fl-border-radius: 4px;
--fl-radius-sm: 0.25rem;
--fl-radius-md: 0.5rem;
--fl-radius-lg: 0.75rem;
--fl-radius-xl: 1rem;
--fl-radius-full: 9999px;
// Shadows
--fl-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
--fl-shadow-dark: 0 4px 12px rgba(0, 0, 0, 0.35);
--fl-transition: 0.4s cubic-bezier(0.23, 1, 0.32, 1);
--fl-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--fl-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--fl-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
// Animation
--fl-transition: 0.4s cubic-bezier(0.23, 1, 0.32, 1);
--fl-duration-fast: 0.15s;
--fl-duration-base: 0.3s;
--fl-duration-slow: 0.5s;
--fl-easing-spring: cubic-bezier(0.23, 1, 0.32, 1);
--fl-easing-material: cubic-bezier(0.4, 0, 0.2, 1);
// Animation distances
--fl-slide-sm: 8px;
--fl-slide-md: 12px;
--fl-slide-lg: 20px;
// Legacy aliases (for backwards compatibility)
--background-color: var(--fl-bg-light);
--text-color: var(--fl-text-light);
--dark-background-color: var(--fl-bg-dark);
+17 -38
View File
@@ -1,59 +1,38 @@
import './ios.scss'
import type { Envelope } from '../../types'
import { getTypeIcon } from '../shared/icons'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
const APP_NAME = 'PHPFlasher'
function getTimeString(): string {
const now = new Date()
return now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
}
export const iosTheme = {
render: (envelope: Envelope): string => {
const { type, message, title } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
const appName = 'PHPFlasher'
const now = new Date()
const timeString = now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
const getIcon = () => {
switch (type) {
case 'success':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>`
case 'error':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>`
case 'warning':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>`
case 'info':
return `<svg class="fl-icon-svg" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>`
}
return ''
}
const displayTitle = title || appName
const displayTitle = title || APP_NAME
return `
<div class="fl-ios fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('ios')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-ios-notification">
<div class="fl-header">
<div class="fl-app-icon">
${getIcon()}
${getTypeIcon(type, { size: 'md', className: 'fl-icon-svg' })}
</div>
<div class="fl-app-info">
<div class="fl-app-name">${displayTitle}</div>
<div class="fl-time">${timeString}</div>
<div class="fl-time">${getTimeString()}</div>
</div>
</div>
<div class="fl-content">
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
<button class="fl-close" aria-label="Close ${type} message">
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
<span aria-hidden="true">×</span>
</button>
</div>
+8 -10
View File
@@ -1,22 +1,20 @@
import './jade.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const jadeTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-jade fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-message">${message}</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<div class="${CLASS_NAMES.theme('jade')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
@@ -1,32 +1,28 @@
import './material.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES, DEFAULT_TEXT } from '../shared/constants'
export const materialTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
const actionText = 'DISMISS'
return `
<div class="fl-material fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('material')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-md-card">
<div class="fl-content">
<div class="${CLASS_NAMES.content}">
<div class="fl-text-content">
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
</div>
<div class="fl-actions">
<button class="fl-action-button fl-close" aria-label="Close ${type} message">
${actionText}
<div class="${CLASS_NAMES.actions}">
<button class="fl-action-button ${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
${DEFAULT_TEXT.dismissButton}
</button>
</div>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
@@ -1,22 +1,20 @@
import './minimal.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const minimalTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-minimal fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-message">${message}</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<div class="${CLASS_NAMES.theme('minimal')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
+101 -5
View File
@@ -8,26 +8,122 @@
}
}
// Legacy close button mixin (for backwards compatibility)
@mixin close-button {
.fl-close {
position: absolute;
right: 0.75rem;
right: var(--fl-spacing-md, 0.75rem);
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
font-size: 1.25rem;
font-size: var(--fl-font-size-xl, 1.25rem);
line-height: 1;
padding: 0.25rem;
padding: var(--fl-spacing-xs, 0.25rem);
cursor: pointer;
opacity: 0.5;
transition: opacity 0.2s ease;
transition: opacity var(--fl-duration-fast, 0.15s) ease;
color: currentColor;
touch-action: manipulation;
&:hover, &:focus {
opacity: 1;
}
&:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
}
}
// Parameterized close button with size variants
@mixin close-button-sized($size: 'md', $position: 'absolute') {
.fl-close {
@if $position == 'absolute' {
position: absolute;
right: var(--fl-spacing-md, 0.75rem);
top: 50%;
transform: translateY(-50%);
} @else {
position: relative;
}
background: none;
border: none;
cursor: pointer;
color: currentColor;
opacity: 0.5;
transition: opacity var(--fl-duration-fast, 0.15s) ease,
transform var(--fl-duration-fast, 0.15s) ease;
touch-action: manipulation;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
@if $size == 'sm' {
width: var(--fl-close-sm, 1.125rem);
height: var(--fl-close-sm, 1.125rem);
font-size: var(--fl-font-size-base, 1rem);
} @else if $size == 'lg' {
width: var(--fl-close-lg, 1.875rem);
height: var(--fl-close-lg, 1.875rem);
font-size: var(--fl-font-size-xl, 1.25rem);
} @else {
width: var(--fl-close-md, 1.5rem);
height: var(--fl-close-md, 1.5rem);
font-size: var(--fl-font-size-lg, 1.125rem);
}
&:hover, &:focus {
opacity: 1;
}
&:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
}
}
// Circular close button variant
@mixin close-button-circular($size: 'md') {
@include close-button-sized($size, 'relative');
.fl-close {
border-radius: var(--fl-radius-full, 9999px);
&:hover, &:focus {
background-color: rgba(0, 0, 0, 0.05);
}
}
}
// Text-style close button (e.g., "DISMISS")
@mixin close-button-text {
.fl-close {
background: transparent;
border: none;
font-family: inherit;
font-weight: 500;
font-size: var(--fl-font-size-sm, 0.875rem);
text-transform: uppercase;
letter-spacing: 0.05em;
padding: var(--fl-spacing-sm, 0.5rem) var(--fl-spacing-md, 0.75rem);
cursor: pointer;
color: currentColor;
border-radius: var(--fl-radius-sm, 0.25rem);
transition: background-color var(--fl-duration-fast, 0.15s) ease;
&:hover, &:focus {
background-color: rgba(0, 0, 0, 0.04);
}
&:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
}
}
@@ -39,7 +135,7 @@
.fl-close {
right: auto;
left: 0.75rem;
left: var(--fl-spacing-md, 0.75rem);
}
}
}
+8 -10
View File
@@ -1,22 +1,20 @@
import './neon.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const neonTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-neon fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-message">${message}</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<div class="${CLASS_NAMES.theme('neon')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
+9 -11
View File
@@ -1,24 +1,22 @@
import './onyx.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const onyxTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-onyx fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-text">
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.theme('onyx')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.text}">
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
+10 -12
View File
@@ -1,28 +1,26 @@
import './ruby.scss'
import type { Envelope } from '../../types'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const rubyTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-ruby fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('ruby')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-shine"></div>
<div class="fl-content">
<div class="${CLASS_NAMES.content}">
<div class="fl-icon-circle">
<div class="fl-icon"></div>
<div class="${CLASS_NAMES.icon}"></div>
</div>
<div class="fl-text">
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.text}">
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
@@ -1,21 +1,19 @@
import './sapphire.scss'
import type { Envelope } from '../../types'
import { getA11yString } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
export const sapphireTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
return `
<div class="fl-sapphire fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<span class="fl-message">${message}</span>
<div class="${CLASS_NAMES.theme('sapphire')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<span class="${CLASS_NAMES.message}">${message}</span>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`
},
@@ -0,0 +1,25 @@
export type NotificationType = 'success' | 'info' | 'warning' | 'error'
export interface A11yAttributes {
role: 'alert' | 'status'
ariaLive: 'assertive' | 'polite'
ariaAtomic: 'true'
}
export function getA11yAttributes(type: NotificationType | string): A11yAttributes {
const isAlert = type === 'error' || type === 'warning'
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
}
}
export function getA11yString(type: NotificationType | string): string {
const attrs = getA11yAttributes(type)
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`
}
export function getCloseButtonA11y(type: NotificationType | string): string {
return `aria-label="Close ${type} message"`
}
@@ -0,0 +1,45 @@
export const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type: string) => `fl-${type}`,
theme: (name: string) => `fl-${name}`,
} as const
export const DEFAULT_TITLES: Record<string, string> = {
success: 'Success',
error: 'Error',
warning: 'Warning',
info: 'Information',
}
export const DEFAULT_TEXT = {
dismissButton: 'DISMISS',
closeLabel: (type: string) => `Close ${type} message`,
} as const
export function capitalizeType(type: string): string {
return type.charAt(0).toUpperCase() + type.slice(1)
}
export function getTitle(title: string | undefined, type: string): string {
return title || DEFAULT_TITLES[type] || capitalizeType(type)
}
@@ -0,0 +1,51 @@
export type IconType = 'success' | 'info' | 'warning' | 'error' | 'close'
export type IconSize = 'sm' | 'md' | 'lg' | number
export interface IconConfig {
size?: IconSize
className?: string
}
const sizeMap: Record<string, number> = {
sm: 16,
md: 20,
lg: 24,
}
const iconPaths: Record<IconType, string> = {
success:
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
error: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
warning:
'M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z',
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
}
export function getIcon(type: IconType, config: IconConfig = {}): string {
const { size = 'md', className = '' } = config
const dimension = typeof size === 'number' ? size : sizeMap[size]
const path = iconPaths[type]
if (!path) {
return ''
}
const classAttr = className ? ` class="${className}"` : ''
return `<svg${classAttr} viewBox="0 0 24 24" width="${dimension}" height="${dimension}" aria-hidden="true"><path fill="currentColor" d="${path}"/></svg>`
}
export function getCloseIcon(config: IconConfig = {}): string {
return getIcon('close', { size: 'sm', ...config })
}
export function getTypeIcon(
type: string,
config: IconConfig = {}
): string {
if (type === 'success' || type === 'error' || type === 'warning' || type === 'info') {
return getIcon(type, config)
}
return ''
}
@@ -0,0 +1,13 @@
export { getIcon, getCloseIcon, getTypeIcon } from './icons'
export type { IconType, IconSize, IconConfig } from './icons'
export { getA11yAttributes, getA11yString, getCloseButtonA11y } from './accessibility'
export type { NotificationType, A11yAttributes } from './accessibility'
export {
CLASS_NAMES,
DEFAULT_TITLES,
DEFAULT_TEXT,
capitalizeType,
getTitle,
} from './constants'
@@ -1,42 +1,34 @@
import './slack.scss'
import type { Envelope } from '../../types'
import { getCloseIcon } from '../shared/icons'
import { getA11yString, getCloseButtonA11y } from '../shared/accessibility'
import { CLASS_NAMES } from '../shared/constants'
const TYPE_ICONS: Record<string, string> = {
success: '✓',
error: '✕',
warning: '!',
info: 'i',
}
export const slackTheme = {
render: (envelope: Envelope): string => {
const { type, message } = envelope
const isAlert = type === 'error' || type === 'warning'
const role = isAlert ? 'alert' : 'status'
const ariaLive = isAlert ? 'assertive' : 'polite'
const getTypeIcon = () => {
switch (type) {
case 'success':
return `<div class="fl-type-icon fl-success-icon">✓</div>`
case 'error':
return `<div class="fl-type-icon fl-error-icon">✕</div>`
case 'warning':
return `<div class="fl-type-icon fl-warning-icon">!</div>`
case 'info':
return `<div class="fl-type-icon fl-info-icon">i</div>`
}
return ''
}
const iconChar = TYPE_ICONS[type] || ''
return `
<div class="fl-slack fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('slack')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-slack-message">
<div class="fl-avatar">
${getTypeIcon()}
<div class="fl-type-icon fl-${type}-icon">${iconChar}</div>
</div>
<div class="fl-message-content">
<div class="fl-message-text">${message}</div>
</div>
<div class="fl-actions">
<button class="fl-close" aria-label="Close ${type} message">
<svg viewBox="0 0 20 20" width="16" height="16">
<path fill="currentColor" d="M10 8.586L6.707 5.293a1 1 0 00-1.414 1.414L8.586 10l-3.293 3.293a1 1 0 101.414 1.414L10 11.414l3.293 3.293a1 1 0 001.414-1.414L11.414 10l3.293-3.293a1 1 0 00-1.414-1.414L10 8.586z"/>
</svg>
<div class="${CLASS_NAMES.actions}">
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
${getCloseIcon()}
</button>
</div>
</div>
+2
View File
@@ -14,4 +14,6 @@ export default class FlasherPlugin extends AbstractPlugin {
private removeNotification;
private stringToHTML;
private escapeHtml;
private dispatchClickEvents;
private getThemeName;
}
+130 -27
View File
@@ -170,7 +170,11 @@ class FlasherPlugin extends AbstractPlugin {
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', render);
const onDOMReady = () => {
document.removeEventListener('DOMContentLoaded', onDOMReady);
render();
};
document.addEventListener('DOMContentLoaded', onDOMReady);
}
else {
render();
@@ -190,7 +194,8 @@ class FlasherPlugin extends AbstractPlugin {
container.dataset.position = options.position;
Object.entries(options.style).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
container.style.setProperty(key, String(value));
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
container.style.setProperty(cssKey, String(value));
}
});
document.body.appendChild(container);
@@ -222,6 +227,12 @@ class FlasherPlugin extends AbstractPlugin {
this.removeNotification(notification);
});
}
notification.addEventListener('click', (event) => {
if (event.target.closest('.fl-close')) {
return;
}
this.dispatchClickEvents(envelope);
});
if (options.timeout > 0) {
this.addTimer(notification, options);
}
@@ -271,16 +282,27 @@ class FlasherPlugin extends AbstractPlugin {
}
};
intervalId = window.setInterval(updateTimer, lapse);
notification.addEventListener('mouseout', () => {
const handleMouseOut = () => {
clearInterval(intervalId);
intervalId = window.setInterval(updateTimer, lapse);
});
notification.addEventListener('mouseover', () => clearInterval(intervalId));
};
const handleMouseOver = () => clearInterval(intervalId);
notification.addEventListener('mouseout', handleMouseOut);
notification.addEventListener('mouseover', handleMouseOver);
notification._flasherCleanup = () => {
clearInterval(intervalId);
notification.removeEventListener('mouseout', handleMouseOut);
notification.removeEventListener('mouseover', handleMouseOver);
};
}
removeNotification(notification) {
if (!notification) {
return;
}
if (notification._flasherCleanup) {
notification._flasherCleanup();
delete notification._flasherCleanup;
}
notification.classList.remove('fl-show');
notification.ontransitionend = () => {
const parent = notification.parentElement;
@@ -293,7 +315,11 @@ class FlasherPlugin extends AbstractPlugin {
stringToHTML(str) {
const template = document.createElement('template');
template.innerHTML = str.trim();
return template.content.firstElementChild;
const element = template.content.firstElementChild;
if (!element) {
throw new Error('PHPFlasher: Invalid HTML template - no element found');
}
return element;
}
escapeHtml(str) {
if (str == null) {
@@ -311,6 +337,25 @@ class FlasherPlugin extends AbstractPlugin {
};
return str.replace(/[&<>"'`=/]/g, (char) => htmlEscapes[char] || char);
}
dispatchClickEvents(envelope) {
const detail = { envelope };
window.dispatchEvent(new CustomEvent('flasher:theme:click', { detail }));
const themeName = this.getThemeName(envelope);
if (themeName) {
window.dispatchEvent(new CustomEvent(`flasher:theme:${themeName}:click`, { detail }));
}
}
getThemeName(envelope) {
var _a;
const plugin = ((_a = envelope.metadata) === null || _a === void 0 ? void 0 : _a.plugin) || '';
if (plugin.startsWith('theme.')) {
return plugin.replace('theme.', '');
}
if (plugin === 'flasher') {
return 'flasher';
}
return plugin;
}
}
class Flasher extends AbstractPlugin {
@@ -413,7 +458,7 @@ class Flasher extends AbstractPlugin {
envelope.metadata.plugin = this.resolvePluginAlias(envelope.metadata.plugin);
this.addThemeStyles(resolved, envelope.metadata.plugin);
envelope.options = this.resolveOptions(envelope.options);
envelope.context = response.context;
envelope.context = resolved.context;
});
return resolved;
}
@@ -428,20 +473,32 @@ class Flasher extends AbstractPlugin {
return resolved;
}
resolveFunction(func) {
var _a, _b;
var _a, _b, _c, _d;
if (typeof func !== 'string') {
return func;
}
const functionRegex = /^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/;
const arrowFunctionRegex = /^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/;
const match = func.match(functionRegex) || func.match(arrowFunctionRegex);
if (!match) {
const functionMatch = func.match(functionRegex);
const arrowMatch = func.match(arrowFunctionRegex);
if (!functionMatch && !arrowMatch) {
return func;
}
const args = (_b = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim())) !== null && _b !== void 0 ? _b : [];
let body = match[3].trim();
if (!body.startsWith('{')) {
body = `{ return ${body}; }`;
let args;
let body;
if (functionMatch) {
args = (_b = (_a = functionMatch[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _b !== void 0 ? _b : [];
body = functionMatch[3].trim();
}
else {
args = (_d = (_c = arrowMatch[2]) === null || _c === void 0 ? void 0 : _c.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _d !== void 0 ? _d : [];
body = arrowMatch[3].trim();
if (!body.startsWith('{')) {
body = `return ${body};`;
}
else {
body = body.slice(1, -1).trim();
}
}
try {
return new Function(...args, body);
@@ -505,7 +562,8 @@ class Flasher extends AbstractPlugin {
});
}
loadAsset(url, nonce, type) {
if (document.querySelector(`${type === 'style' ? 'link' : 'script'}[src="${url}"]`)) {
const selector = type === 'style' ? `link[href="${url}"]` : `script[src="${url}"]`;
if (document.querySelector(selector)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
@@ -540,25 +598,70 @@ class Flasher extends AbstractPlugin {
}
}
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const DEFAULT_TITLES = {
success: 'Success',
error: 'Error',
warning: 'Warning',
info: 'Information',
};
function capitalizeType(type) {
return type.charAt(0).toUpperCase() + type.slice(1);
}
function getTitle(title, type) {
return title || DEFAULT_TITLES[type] || capitalizeType(type);
}
const flasherTheme = {
render: (envelope) => {
const { type, title, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
const displayTitle = title || type.charAt(0).toUpperCase() + type.slice(1);
const displayTitle = getTitle(title, type);
return `
<div class="fl-flasher fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-icon"></div>
<div class="${CLASS_NAMES.theme('flasher')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.icon}"></div>
<div>
<strong class="fl-title">${displayTitle}</strong>
<span class="fl-message">${message}</span>
<strong class="${CLASS_NAMES.title}">${displayTitle}</strong>
<span class="${CLASS_NAMES.message}">${message}</span>
</div>
<button class="fl-close" aria-label="Close ${type} message">&times;</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>&times;</button>
</div>
<span class="fl-progress-bar">
<span class="fl-progress"></span>
<span class="${CLASS_NAMES.progressBar}">
<span class="${CLASS_NAMES.progress}"></span>
</span>
</div>`;
},
+130 -27
View File
@@ -176,7 +176,11 @@
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', render);
const onDOMReady = () => {
document.removeEventListener('DOMContentLoaded', onDOMReady);
render();
};
document.addEventListener('DOMContentLoaded', onDOMReady);
}
else {
render();
@@ -196,7 +200,8 @@
container.dataset.position = options.position;
Object.entries(options.style).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
container.style.setProperty(key, String(value));
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
container.style.setProperty(cssKey, String(value));
}
});
document.body.appendChild(container);
@@ -228,6 +233,12 @@
this.removeNotification(notification);
});
}
notification.addEventListener('click', (event) => {
if (event.target.closest('.fl-close')) {
return;
}
this.dispatchClickEvents(envelope);
});
if (options.timeout > 0) {
this.addTimer(notification, options);
}
@@ -277,16 +288,27 @@
}
};
intervalId = window.setInterval(updateTimer, lapse);
notification.addEventListener('mouseout', () => {
const handleMouseOut = () => {
clearInterval(intervalId);
intervalId = window.setInterval(updateTimer, lapse);
});
notification.addEventListener('mouseover', () => clearInterval(intervalId));
};
const handleMouseOver = () => clearInterval(intervalId);
notification.addEventListener('mouseout', handleMouseOut);
notification.addEventListener('mouseover', handleMouseOver);
notification._flasherCleanup = () => {
clearInterval(intervalId);
notification.removeEventListener('mouseout', handleMouseOut);
notification.removeEventListener('mouseover', handleMouseOver);
};
}
removeNotification(notification) {
if (!notification) {
return;
}
if (notification._flasherCleanup) {
notification._flasherCleanup();
delete notification._flasherCleanup;
}
notification.classList.remove('fl-show');
notification.ontransitionend = () => {
const parent = notification.parentElement;
@@ -299,7 +321,11 @@
stringToHTML(str) {
const template = document.createElement('template');
template.innerHTML = str.trim();
return template.content.firstElementChild;
const element = template.content.firstElementChild;
if (!element) {
throw new Error('PHPFlasher: Invalid HTML template - no element found');
}
return element;
}
escapeHtml(str) {
if (str == null) {
@@ -317,6 +343,25 @@
};
return str.replace(/[&<>"'`=/]/g, (char) => htmlEscapes[char] || char);
}
dispatchClickEvents(envelope) {
const detail = { envelope };
window.dispatchEvent(new CustomEvent('flasher:theme:click', { detail }));
const themeName = this.getThemeName(envelope);
if (themeName) {
window.dispatchEvent(new CustomEvent(`flasher:theme:${themeName}:click`, { detail }));
}
}
getThemeName(envelope) {
var _a;
const plugin = ((_a = envelope.metadata) === null || _a === void 0 ? void 0 : _a.plugin) || '';
if (plugin.startsWith('theme.')) {
return plugin.replace('theme.', '');
}
if (plugin === 'flasher') {
return 'flasher';
}
return plugin;
}
}
class Flasher extends AbstractPlugin {
@@ -419,7 +464,7 @@
envelope.metadata.plugin = this.resolvePluginAlias(envelope.metadata.plugin);
this.addThemeStyles(resolved, envelope.metadata.plugin);
envelope.options = this.resolveOptions(envelope.options);
envelope.context = response.context;
envelope.context = resolved.context;
});
return resolved;
}
@@ -434,20 +479,32 @@
return resolved;
}
resolveFunction(func) {
var _a, _b;
var _a, _b, _c, _d;
if (typeof func !== 'string') {
return func;
}
const functionRegex = /^function\s*(\w*)\s*\(([^)]*)\)\s*\{([\s\S]*)\}$/;
const arrowFunctionRegex = /^\s*(\(([^)]*)\)|[^=]+)\s*=>\s*([\s\S]+)$/;
const match = func.match(functionRegex) || func.match(arrowFunctionRegex);
if (!match) {
const functionMatch = func.match(functionRegex);
const arrowMatch = func.match(arrowFunctionRegex);
if (!functionMatch && !arrowMatch) {
return func;
}
const args = (_b = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim())) !== null && _b !== void 0 ? _b : [];
let body = match[3].trim();
if (!body.startsWith('{')) {
body = `{ return ${body}; }`;
let args;
let body;
if (functionMatch) {
args = (_b = (_a = functionMatch[2]) === null || _a === void 0 ? void 0 : _a.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _b !== void 0 ? _b : [];
body = functionMatch[3].trim();
}
else {
args = (_d = (_c = arrowMatch[2]) === null || _c === void 0 ? void 0 : _c.split(',').map((arg) => arg.trim()).filter(Boolean)) !== null && _d !== void 0 ? _d : [];
body = arrowMatch[3].trim();
if (!body.startsWith('{')) {
body = `return ${body};`;
}
else {
body = body.slice(1, -1).trim();
}
}
try {
return new Function(...args, body);
@@ -511,7 +568,8 @@
});
}
loadAsset(url, nonce, type) {
if (document.querySelector(`${type === 'style' ? 'link' : 'script'}[src="${url}"]`)) {
const selector = type === 'style' ? `link[href="${url}"]` : `script[src="${url}"]`;
if (document.querySelector(selector)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
@@ -546,25 +604,70 @@
}
}
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const DEFAULT_TITLES = {
success: 'Success',
error: 'Error',
warning: 'Warning',
info: 'Information',
};
function capitalizeType(type) {
return type.charAt(0).toUpperCase() + type.slice(1);
}
function getTitle(title, type) {
return title || DEFAULT_TITLES[type] || capitalizeType(type);
}
const flasherTheme = {
render: (envelope) => {
const { type, title, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
const displayTitle = title || type.charAt(0).toUpperCase() + type.slice(1);
const displayTitle = getTitle(title, type);
return `
<div class="fl-flasher fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-icon"></div>
<div class="${CLASS_NAMES.theme('flasher')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.icon}"></div>
<div>
<strong class="fl-title">${displayTitle}</strong>
<span class="fl-message">${message}</span>
<strong class="${CLASS_NAMES.title}">${displayTitle}</strong>
<span class="${CLASS_NAMES.message}">${message}</span>
</div>
<button class="fl-close" aria-label="Close ${type} message">&times;</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>&times;</button>
</div>
<span class="fl-progress-bar">
<span class="fl-progress"></span>
<span class="${CLASS_NAMES.progressBar}">
<span class="${CLASS_NAMES.progress}"></span>
</span>
</div>`;
},
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+86 -40
View File
@@ -5,59 +5,105 @@
*/
import flasher from '@flasher/flasher';
const sizeMap = {
sm: 16,
md: 20,
lg: 24,
};
const iconPaths = {
success: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
error: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
warning: 'M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z',
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
};
function getIcon(type, config = {}) {
const { size = 'md', className = '' } = config;
const dimension = typeof size === 'number' ? size : sizeMap[size];
const path = iconPaths[type];
if (!path) {
return '';
}
const classAttr = className ? ` class="${className}"` : '';
return `<svg${classAttr} viewBox="0 0 24 24" width="${dimension}" height="${dimension}" aria-hidden="true"><path fill="currentColor" d="${path}"/></svg>`;
}
function getCloseIcon(config = {}) {
return getIcon('close', Object.assign({ size: 'sm' }, config));
}
function getTypeIcon(type, config = {}) {
if (type === 'success' || type === 'error' || type === 'warning' || type === 'info') {
return getIcon(type, config);
}
return '';
}
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const DEFAULT_TITLES = {
success: 'Success',
error: 'Error',
warning: 'Warning',
info: 'Information',
};
const AMAZON_TITLES = {
success: 'Success!',
error: 'Problem',
warning: 'Warning',
info: 'Information',
};
const amazonTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
const getAlertIcon = () => {
switch (type) {
case 'success':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>`;
case 'error':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>`;
case 'warning':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>`;
case 'info':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>`;
}
return '';
};
const getAlertTitle = () => {
switch (type) {
case 'success': return 'Success!';
case 'error': return 'Problem';
case 'warning': return 'Warning';
case 'info': return 'Information';
default: return 'Alert';
}
};
const alertTitle = AMAZON_TITLES[type] || DEFAULT_TITLES[type] || 'Alert';
return `
<div class="fl-amazon fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('amazon')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-amazon-alert">
<div class="fl-alert-content">
<div class="fl-icon-container">
${getAlertIcon()}
${getTypeIcon(type, { size: 'lg' })}
</div>
<div class="fl-text-content">
<div class="fl-alert-title">${getAlertTitle()}</div>
<div class="fl-alert-title">${alertTitle}</div>
<div class="fl-alert-message">${message}</div>
</div>
</div>
<div class="fl-alert-actions">
<button class="fl-close" aria-label="Close notification">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
${getCloseIcon()}
</button>
</div>
</div>
+86 -40
View File
@@ -9,59 +9,105 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.flasher));
})(this, (function (flasher) { 'use strict';
const sizeMap = {
sm: 16,
md: 20,
lg: 24,
};
const iconPaths = {
success: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z',
error: 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
warning: 'M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z',
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
close: 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z',
};
function getIcon(type, config = {}) {
const { size = 'md', className = '' } = config;
const dimension = typeof size === 'number' ? size : sizeMap[size];
const path = iconPaths[type];
if (!path) {
return '';
}
const classAttr = className ? ` class="${className}"` : '';
return `<svg${classAttr} viewBox="0 0 24 24" width="${dimension}" height="${dimension}" aria-hidden="true"><path fill="currentColor" d="${path}"/></svg>`;
}
function getCloseIcon(config = {}) {
return getIcon('close', Object.assign({ size: 'sm' }, config));
}
function getTypeIcon(type, config = {}) {
if (type === 'success' || type === 'error' || type === 'warning' || type === 'info') {
return getIcon(type, config);
}
return '';
}
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const DEFAULT_TITLES = {
success: 'Success',
error: 'Error',
warning: 'Warning',
info: 'Information',
};
const AMAZON_TITLES = {
success: 'Success!',
error: 'Problem',
warning: 'Warning',
info: 'Information',
};
const amazonTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
const getAlertIcon = () => {
switch (type) {
case 'success':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>`;
case 'error':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>`;
case 'warning':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>`;
case 'info':
return `<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>`;
}
return '';
};
const getAlertTitle = () => {
switch (type) {
case 'success': return 'Success!';
case 'error': return 'Problem';
case 'warning': return 'Warning';
case 'info': return 'Information';
default: return 'Alert';
}
};
const alertTitle = AMAZON_TITLES[type] || DEFAULT_TITLES[type] || 'Alert';
return `
<div class="fl-amazon fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="${CLASS_NAMES.theme('amazon')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="fl-amazon-alert">
<div class="fl-alert-content">
<div class="fl-icon-container">
${getAlertIcon()}
${getTypeIcon(type, { size: 'lg' })}
</div>
<div class="fl-text-content">
<div class="fl-alert-title">${getAlertTitle()}</div>
<div class="fl-alert-title">${alertTitle}</div>
<div class="fl-alert-message">${message}</div>
</div>
</div>
<div class="fl-alert-actions">
<button class="fl-close" aria-label="Close notification">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>
${getCloseIcon()}
</button>
</div>
</div>
+1 -1
View File
@@ -1 +1 @@
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).flasher)}(this,(function(e){"use strict";e.addTheme("amazon",{render:e=>{const{type:n,message:t}=e,r="error"===n||"warning"===n;return`\n <div class="fl-amazon fl-${n}" role="${r?"alert":"status"}" aria-live="${r?"assertive":"polite"}" aria-atomic="true">\n <div class="fl-amazon-alert">\n <div class="fl-alert-content">\n <div class="fl-icon-container">\n ${(()=>{switch(n){case"success":return'<svg viewBox="0 0 24 24" width="24" height="24">\n <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>\n </svg>';case"error":return'<svg viewBox="0 0 24 24" width="24" height="24">\n <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>\n </svg>';case"warning":return'<svg viewBox="0 0 24 24" width="24" height="24">\n <path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>\n </svg>';case"info":return'<svg viewBox="0 0 24 24" width="24" height="24">\n <path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>\n </svg>'}return""})()}\n </div>\n <div class="fl-text-content">\n <div class="fl-alert-title">${(()=>{switch(n){case"success":return"Success!";case"error":return"Problem";case"warning":return"Warning";case"info":return"Information";default:return"Alert"}})()}</div>\n <div class="fl-alert-message">${t}</div>\n </div>\n </div>\n <div class="fl-alert-actions">\n <button class="fl-close" aria-label="Close notification">\n <svg viewBox="0 0 24 24" width="16" height="16">\n <path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>\n </svg>\n </button>\n </div>\n </div>\n </div>`}})}));
!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],e):e((n="undefined"!=typeof globalThis?globalThis:n||self).flasher)}(this,(function(n){"use strict";const e={sm:16,md:20,lg:24},s={success:"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z",error:"M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z",warning:"M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z",info:"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z",close:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"};function r(n,r={}){const{size:i="md",className:t=""}=r,a="number"==typeof i?i:e[i],o=s[n];if(!o)return"";return`<svg${t?` class="${t}"`:""} viewBox="0 0 24 24" width="${a}" height="${a}" aria-hidden="true"><path fill="currentColor" d="${o}"/></svg>`}const i="fl-close",t=n=>`fl-${n}`,a=n=>`fl-${n}`,o={success:"Success",error:"Error",warning:"Warning",info:"Information"},l={success:"Success!",error:"Problem",warning:"Warning",info:"Information"},c={render:n=>{const{type:e,message:s}=n,c=l[e]||o[e]||"Alert";return`\n <div class="${a("amazon")} ${t(e)}" ${function(n){const e=function(n){const e="error"===n||"warning"===n;return{role:e?"alert":"status",ariaLive:e?"assertive":"polite",ariaAtomic:"true"}}(n);return`role="${e.role}" aria-live="${e.ariaLive}" aria-atomic="${e.ariaAtomic}"`}(e)}>\n <div class="fl-amazon-alert">\n <div class="fl-alert-content">\n <div class="fl-icon-container">\n ${function(n,e={}){return"success"===n||"error"===n||"warning"===n||"info"===n?r(n,e):""}(e,{size:"lg"})}\n </div>\n <div class="fl-text-content">\n <div class="fl-alert-title">${c}</div>\n <div class="fl-alert-message">${s}</div>\n </div>\n </div>\n <div class="fl-alert-actions">\n <button class="${i}" ${function(n){return`aria-label="Close ${n} message"`}(e)}>\n ${function(n={}){return r("close",Object.assign({size:"sm"},n))}()}\n </button>\n </div>\n </div>\n </div>`}};n.addTheme("amazon",c)}));
+44 -11
View File
@@ -5,23 +5,56 @@
*/
import flasher from '@flasher/flasher';
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const amberTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
return `
<div class="fl-amber fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-icon"></div>
<div class="fl-text">
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.theme('amber')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.icon}"></div>
<div class="${CLASS_NAMES.text}">
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`;
},
+44 -11
View File
@@ -9,23 +9,56 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.flasher));
})(this, (function (flasher) { 'use strict';
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const amberTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
return `
<div class="fl-amber fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-icon"></div>
<div class="fl-text">
<div class="fl-message">${message}</div>
<div class="${CLASS_NAMES.theme('amber')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.icon}"></div>
<div class="${CLASS_NAMES.text}">
<div class="${CLASS_NAMES.message}">${message}</div>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`;
},
+1 -1
View File
@@ -1 +1 @@
!function(e,s){"object"==typeof exports&&"undefined"!=typeof module?s(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],s):s((e="undefined"!=typeof globalThis?globalThis:e||self).flasher)}(this,(function(e){"use strict";e.addTheme("amber",{render:e=>{const{type:s,message:i}=e,l="error"===s||"warning"===s;return`\n <div class="fl-amber fl-${s}" role="${l?"alert":"status"}" aria-live="${l?"assertive":"polite"}" aria-atomic="true">\n <div class="fl-content">\n <div class="fl-icon"></div>\n <div class="fl-text">\n <div class="fl-message">${i}</div>\n </div>\n <button class="fl-close" aria-label="Close ${s} message">×</button>\n </div>\n <div class="fl-progress-bar">\n <div class="fl-progress"></div>\n </div>\n </div>`}})}));
!function(e,s){"object"==typeof exports&&"undefined"!=typeof module?s(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],s):s((e="undefined"!=typeof globalThis?globalThis:e||self).flasher)}(this,(function(e){"use strict";const s="fl-content",i="fl-message",n="fl-text",a="fl-icon",r="fl-close",t="fl-progress-bar",l="fl-progress",o=e=>`fl-${e}`,f=e=>`fl-${e}`,c={render:e=>{const{type:c,message:d}=e;return`\n <div class="${f("amber")} ${o(c)}" ${function(e){const s=function(e){const s="error"===e||"warning"===e;return{role:s?"alert":"status",ariaLive:s?"assertive":"polite",ariaAtomic:"true"}}(e);return`role="${s.role}" aria-live="${s.ariaLive}" aria-atomic="${s.ariaAtomic}"`}(c)}>\n <div class="${s}">\n <div class="${a}"></div>\n <div class="${n}">\n <div class="${i}">${d}</div>\n </div>\n <button class="${r}" ${function(e){return`aria-label="Close ${e} message"`}(c)}>×</button>\n </div>\n <div class="${t}">\n <div class="${l}"></div>\n </div>\n </div>`}};e.addTheme("amber",c)}));
+42 -9
View File
@@ -5,20 +5,53 @@
*/
import flasher from '@flasher/flasher';
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const auroraTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
return `
<div class="fl-aurora fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-message">${message}</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<div class="${CLASS_NAMES.theme('aurora')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`;
},
+42 -9
View File
@@ -9,20 +9,53 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.flasher));
})(this, (function (flasher) { 'use strict';
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const auroraTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
return `
<div class="fl-aurora fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-message">${message}</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<div class="${CLASS_NAMES.theme('aurora')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.message}">${message}</div>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`;
},
+1 -1
View File
@@ -1 +1 @@
!function(e,s){"object"==typeof exports&&"undefined"!=typeof module?s(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],s):s((e="undefined"!=typeof globalThis?globalThis:e||self).flasher)}(this,(function(e){"use strict";e.addTheme("aurora",{render:e=>{const{type:s,message:a}=e,r="error"===s||"warning"===s;return`\n <div class="fl-aurora fl-${s}" role="${r?"alert":"status"}" aria-live="${r?"assertive":"polite"}" aria-atomic="true">\n <div class="fl-content">\n <div class="fl-message">${a}</div>\n <button class="fl-close" aria-label="Close ${s} message">×</button>\n </div>\n <div class="fl-progress-bar">\n <div class="fl-progress"></div>\n </div>\n </div>`}})}));
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(require("@flasher/flasher")):"function"==typeof define&&define.amd?define(["@flasher/flasher"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).flasher)}(this,(function(e){"use strict";const r="fl-content",s="fl-message",a="fl-close",n="fl-progress-bar",i="fl-progress",t=e=>`fl-${e}`,o=e=>`fl-${e}`,l={render:e=>{const{type:l,message:f}=e;return`\n <div class="${o("aurora")} ${t(l)}" ${function(e){const r=function(e){const r="error"===e||"warning"===e;return{role:r?"alert":"status",ariaLive:r?"assertive":"polite",ariaAtomic:"true"}}(e);return`role="${r.role}" aria-live="${r.ariaLive}" aria-atomic="${r.ariaAtomic}"`}(l)}>\n <div class="${r}">\n <div class="${s}">${f}</div>\n <button class="${a}" ${function(e){return`aria-label="Close ${e} message"`}(l)}>×</button>\n </div>\n <div class="${n}">\n <div class="${i}"></div>\n </div>\n </div>`}};e.addTheme("aurora",l)}));
+43 -10
View File
@@ -5,22 +5,55 @@
*/
import flasher from '@flasher/flasher';
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const crystalTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
return `
<div class="fl-crystal fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-text">
<p class="fl-message">${message}</p>
<div class="${CLASS_NAMES.theme('crystal')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.text}">
<p class="${CLASS_NAMES.message}">${message}</p>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`;
},
+43 -10
View File
@@ -9,22 +9,55 @@
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.flasher));
})(this, (function (flasher) { 'use strict';
function getA11yAttributes(type) {
const isAlert = type === 'error' || type === 'warning';
return {
role: isAlert ? 'alert' : 'status',
ariaLive: isAlert ? 'assertive' : 'polite',
ariaAtomic: 'true',
};
}
function getA11yString(type) {
const attrs = getA11yAttributes(type);
return `role="${attrs.role}" aria-live="${attrs.ariaLive}" aria-atomic="${attrs.ariaAtomic}"`;
}
function getCloseButtonA11y(type) {
return `aria-label="Close ${type} message"`;
}
const CLASS_NAMES = {
container: 'fl-container',
wrapper: 'fl-wrapper',
content: 'fl-content',
message: 'fl-message',
title: 'fl-title',
text: 'fl-text',
icon: 'fl-icon',
iconWrapper: 'fl-icon-wrapper',
actions: 'fl-actions',
close: 'fl-close',
progressBar: 'fl-progress-bar',
progress: 'fl-progress',
show: 'fl-show',
sticky: 'fl-sticky',
rtl: 'fl-rtl',
type: (type) => `fl-${type}`,
theme: (name) => `fl-${name}`,
};
const crystalTheme = {
render: (envelope) => {
const { type, message } = envelope;
const isAlert = type === 'error' || type === 'warning';
const role = isAlert ? 'alert' : 'status';
const ariaLive = isAlert ? 'assertive' : 'polite';
return `
<div class="fl-crystal fl-${type}" role="${role}" aria-live="${ariaLive}" aria-atomic="true">
<div class="fl-content">
<div class="fl-text">
<p class="fl-message">${message}</p>
<div class="${CLASS_NAMES.theme('crystal')} ${CLASS_NAMES.type(type)}" ${getA11yString(type)}>
<div class="${CLASS_NAMES.content}">
<div class="${CLASS_NAMES.text}">
<p class="${CLASS_NAMES.message}">${message}</p>
</div>
<button class="fl-close" aria-label="Close ${type} message">×</button>
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</button>
</div>
<div class="fl-progress-bar">
<div class="fl-progress"></div>
<div class="${CLASS_NAMES.progressBar}">
<div class="${CLASS_NAMES.progress}"></div>
</div>
</div>`;
},

Some files were not shown because too many files have changed in this diff Show More