mirror of
https://github.com/php-flasher/php-flasher.git
synced 2026-03-31 15:07:47 +01:00
Compare commits
54 Commits
c074feea05
...
fd65254d63
| Author | SHA1 | Date | |
|---|---|---|---|
| fd65254d63 | |||
| 33ac9013d5 | |||
| 1cc6a7c537 | |||
| dfe9a12fe1 | |||
| 8074bb1f90 | |||
| 03942aa634 | |||
| 6abae0bdde | |||
| 05c15399ac | |||
| 1bd85021d9 | |||
| 359e6de361 | |||
| e417105f7a | |||
| abd70c1d4b | |||
| 9acddbda52 | |||
| 7d6e9b46b8 | |||
| f1051e1d7f | |||
| 87da42fdea | |||
| 4d9cda22cf | |||
| c58f3c7b40 | |||
| 47eb66e874 | |||
| 30de24f054 | |||
| 162ea87330 | |||
| 8cda9d1eb1 | |||
| 6d314dbc07 | |||
| ad5c0f56dd | |||
| fd36c2ec0c | |||
| 5202c86107 | |||
| 9e7bb17faa | |||
| 83dc9e49dc | |||
| 1d81de581b | |||
| 2ebdbecda6 | |||
| 851e0a00ed | |||
| d9b0b6998e | |||
| ed992d78f6 | |||
| 0612a3bb61 | |||
| c9a61ba69c | |||
| f9746f607b | |||
| 18c2233baa | |||
| 08c242b45a | |||
| cd53ceb139 | |||
| 549c36eeee | |||
| ea0dccc961 | |||
| d33de77835 | |||
| 62848e0fd1 | |||
| c5059adac7 | |||
| 4145b870dd | |||
| 18a31f578b | |||
| f20bdebda0 | |||
| f9807e91e2 | |||
| e35339dca9 | |||
| f9bef40ae6 | |||
| 670e40dc97 | |||
| 2b0e736d28 | |||
| b79902779e | |||
| 8779de6c62 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
/vendor/
|
||||
/node_modules/
|
||||
|
||||
/coverage/
|
||||
|
||||
/.cache/php-cs-fixer/
|
||||
/.cache/phplint/
|
||||
/.cache/phpstan/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 "$@"
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
|
||||
Vendored
+2
-2
@@ -2,10 +2,10 @@
|
||||
"entrypoints": {
|
||||
"main": {
|
||||
"css": [
|
||||
"/dist/main.388db426.css"
|
||||
"/dist/main.b2cdcaf6.css"
|
||||
],
|
||||
"js": [
|
||||
"/dist/main.943ce8e8.js"
|
||||
"/dist/main.29f69f71.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-2
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -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"><?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"><?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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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">×</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
|
||||
@@ -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>
|
||||
Generated
+2302
-306
File diff suppressed because it is too large
Load Diff
+21
-9
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,34 +1,143 @@
|
||||
# PHPFlasher Noty Adapter
|
||||
|
||||
[](https://packagist.org/packages/php-flasher/flasher-noty)
|
||||
[](https://packagist.org/packages/php-flasher/flasher-noty)
|
||||
[](https://packagist.org/packages/php-flasher/flasher-noty)
|
||||
[](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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
@@ -7,4 +7,6 @@ export default class NotyfPlugin extends AbstractPlugin {
|
||||
renderOptions(options: Options): void;
|
||||
private initializeNotyf;
|
||||
private addTypeIfNotExists;
|
||||
private attachEventListeners;
|
||||
private dispatchEvent;
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+125
-11
@@ -1,34 +1,148 @@
|
||||
# PHPFlasher Notyf Adapter
|
||||
|
||||
[](https://packagist.org/packages/php-flasher/flasher-notyf)
|
||||
[](https://packagist.org/packages/php-flasher/flasher-notyf)
|
||||
[](https://packagist.org/packages/php-flasher/flasher-notyf)
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</button>
|
||||
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</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>`
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>`
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
},
|
||||
|
||||
@@ -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>`
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,4 +14,6 @@ export default class FlasherPlugin extends AbstractPlugin {
|
||||
private removeNotification;
|
||||
private stringToHTML;
|
||||
private escapeHtml;
|
||||
private dispatchClickEvents;
|
||||
private getThemeName;
|
||||
}
|
||||
|
||||
+130
-27
@@ -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">×</button>
|
||||
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</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>`;
|
||||
},
|
||||
|
||||
Vendored
+130
-27
@@ -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">×</button>
|
||||
<button class="${CLASS_NAMES.close}" ${getCloseButtonA11y(type)}>×</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>`;
|
||||
},
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+86
-40
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user