Initial commit: prestashop-entity-selector

Forked from prestashop-target-conditions
Renamed all references from target-conditions to entity-selector
This commit is contained in:
2026-01-26 14:02:54 +00:00
commit a285018e0d
18 changed files with 39802 additions and 0 deletions

574
README.md Executable file
View File

@@ -0,0 +1,574 @@
# PrestaShop Condition Traits
A collection of reusable PHP traits for PrestaShop admin controllers that provide powerful UI widgets for targeting and scheduling.
## Available Traits
| Trait | Purpose |
|-------|---------|
| **TargetConditions** | Multi-criteria targeting (countries, currencies, carriers, groups, languages) |
| **ScheduleConditions** | Time-based scheduling (datetime range, weekly schedule, holiday exclusions) |
## Installation
### Via Composer
```bash
composer require myprestarocks/prestashop-target-conditions
```
### Manual Installation
Copy the package to your module's vendor directory:
```
modules/your_module/vendor/myprestarocks/prestashop-target-conditions/
```
---
# TargetConditions Trait
A powerful multi-criteria targeting widget. Allows users to define conditions based on countries, currencies, carriers, customer groups, and languages.
## Basic Usage
### 1. Include the Trait in Your Admin Controller
```php
<?php
use MyPrestaRocks\TargetConditions\TargetConditions;
class AdminYourModuleController extends ModuleAdminController
{
use TargetConditions;
// Your controller code...
}
```
### 2. Register Assets in setMedia()
```php
public function setMedia($isNewTheme = false)
{
parent::setMedia($isNewTheme);
// Register the target conditions assets
$this->registerTargetConditionsAssets();
}
```
### 3. Add the Widget to Your Form
In your form rendering method (e.g., `renderForm()`), add the widget:
```php
public function renderForm()
{
// Get existing restrictions from database (JSON string or array)
$restrictions = $this->object->restrictions ?? '[]';
$this->fields_form = [
'legend' => [
'title' => $this->l('Configuration'),
],
'input' => [
[
'type' => 'free',
'label' => $this->l('Target Conditions'),
'name' => 'restrictions_widget',
'desc' => $this->l('Define where this item should be available'),
],
],
// ... other fields
];
// Render the widget HTML
$this->fields_value['restrictions_widget'] = $this->renderTargetConditionsHtml(
'restrictions', // Field name for form submission
$restrictions, // Current value (JSON or array)
[
// Configuration options (see below)
]
);
return parent::renderForm();
}
```
### 4. Process the Submitted Data
```php
public function postProcess()
{
if (Tools::isSubmit('submitYourForm')) {
$restrictions = Tools::getValue('restrictions');
// The value is already JSON-encoded from the widget
// Store it in your database
$this->object->restrictions = $restrictions;
$this->object->save();
}
return parent::postProcess();
}
```
## Configuration Options
The `renderTargetConditionsHtml()` method accepts a configuration array as the third parameter:
```php
$this->renderTargetConditionsHtml('field_name', $value, [
// Available condition groups to show
// Default: all groups enabled
'groups' => [
'countries' => true, // Country selection
'currencies' => true, // Currency selection
'carriers' => true, // Carrier/shipping method selection
'groups' => true, // Customer group selection
'languages' => true, // Language selection
],
// Empty selection behavior
// true = empty means "all included" (available everywhere)
// false = empty means "none selected" (not available anywhere)
'empty_means_all' => true,
// Show "All X" toggle badges
// When set, displays toggle buttons showing selection state
'show_all_toggle' => [
'empty' => $this->l('Available Everywhere'), // Label when nothing selected
'selected' => $this->l('Restricted'), // Label when items selected
],
// Show group modifiers (Limit/Sort controls)
// true = show limit dropdown and sort controls per group
// false = hide modifiers (cleaner UI for targeting contexts)
'show_modifiers' => true,
// Pre-filter available options based on external restrictions
// Useful when parent entity has its own restrictions
'restrictions' => [
'countries' => ['FR', 'DE', 'PL'], // Only show these countries
'currencies' => ['EUR', 'PLN'], // Only show these currencies
// ... etc
],
// Custom labels for condition groups
'labels' => [
'countries' => $this->l('Countries'),
'currencies' => $this->l('Currencies'),
'carriers' => $this->l('Carriers'),
'groups' => $this->l('Customer Groups'),
'languages' => $this->l('Languages'),
],
]);
```
## Use Case Examples
### Payment Method Targeting
Restrict payment methods to specific countries/currencies:
```php
$this->renderTargetConditionsHtml('restrictions', $paymentOption->restrictions, [
'groups' => [
'countries' => true,
'currencies' => true,
'carriers' => false, // Not relevant for payments
'groups' => true,
'languages' => false,
],
'empty_means_all' => true,
'show_modifiers' => false, // No need for limit/sort on targeting
'show_all_toggle' => [
'empty' => $this->l('Available Everywhere'),
'selected' => $this->l('Restricted'),
],
]);
```
### Product Selection Widget
Select products with quantity limits:
```php
$this->renderTargetConditionsHtml('selected_products', $rule->products, [
'groups' => [
'products' => true,
],
'empty_means_all' => false, // Must explicitly select products
'show_modifiers' => true, // Allow limiting quantity per selection
]);
```
### Shipping Rules
Define carrier availability by zone:
```php
$this->renderTargetConditionsHtml('availability', $carrier->zones, [
'groups' => [
'countries' => true,
'groups' => true, // Different rates for customer groups
],
'empty_means_all' => true,
'show_all_toggle' => [
'empty' => $this->l('Ships Everywhere'),
'selected' => $this->l('Limited Zones'),
],
]);
```
## Data Format
The widget stores data as a JSON object with the following structure:
```json
{
"countries": {
"items": ["FR", "DE", "PL"],
"limit": 0,
"sort": "default"
},
"currencies": {
"items": ["EUR", "PLN"],
"limit": 0,
"sort": "default"
},
"carriers": {
"items": [],
"limit": 0,
"sort": "default"
},
"groups": {
"items": [1, 2, 3],
"limit": 0,
"sort": "default"
},
"languages": {
"items": [],
"limit": 0,
"sort": "default"
}
}
```
### Field Descriptions
- **items**: Array of selected IDs or ISO codes
- **limit**: Maximum items to use (0 = no limit)
- **sort**: Sort order for items ("default", "asc", "desc", "random")
## Selection Logic
### When `empty_means_all` is `true` (default):
- Empty selection = item available for ALL values in that group
- Selection made = item restricted to ONLY selected values
### When `empty_means_all` is `false`:
- Empty selection = item NOT available (nothing selected)
- Selection made = item available for selected values only
### Restriction Inheritance
When using the `restrictions` config option, the widget will only display options that match the parent restrictions. This is useful for hierarchical configurations where child items cannot exceed parent permissions.
Example: A payment method restricted to France and Germany cannot have a sub-option available in Poland.
---
# ScheduleConditions Trait
A time-based scheduling widget with visual timeline controls. Allows users to define when something should be active using datetime ranges, weekly schedules, and public holiday exclusions.
## Features
- **Datetime Range**: Specific start/end period with datetime pickers
- **Weekly Schedule**: Per-day time ranges with visual timeline sliders
- **Public Holiday Exclusions**: Optional integration with holiday providers
## Basic Usage
### 1. Include the Trait
```php
<?php
use MyPrestaRocks\TargetConditions\ScheduleConditions;
class AdminYourModuleController extends ModuleAdminController
{
use ScheduleConditions;
// Your controller code...
}
```
### 2. Initialize Assets in setMedia()
```php
public function setMedia($isNewTheme = false)
{
parent::setMedia($isNewTheme);
// Initialize schedule conditions (loads CSS + JS)
$this->initScheduleConditions();
}
```
### 3. Render the Widget
```php
public function renderForm()
{
// Get saved schedule data (JSON string from database)
$scheduleJson = $this->object->schedule ?? '{}';
$scheduleData = json_decode($scheduleJson, true) ?: [];
$this->fields_form = [
'legend' => ['title' => $this->l('Configuration')],
'input' => [
[
'type' => 'free',
'label' => $this->l('Schedule'),
'name' => 'schedule_widget',
],
],
];
// Render schedule widget
$this->fields_value['schedule_widget'] = $this->renderScheduleConditionsHtml(
[
'id' => 'my-schedule',
'name_prefix' => 'schedule_',
'title' => $this->l('Availability Schedule'),
'subtitle' => $this->l('Define when this should be active'),
'show_datetime_range' => true,
'show_weekly_schedule' => true,
'show_holiday_exclusions' => true,
],
$scheduleData
);
return parent::renderForm();
}
```
### 4. Process Form Submission
```php
public function postProcess()
{
if (Tools::isSubmit('submitYourForm')) {
// Parse schedule data from form
$scheduleData = $this->parseScheduleConditionsFromPost('schedule_');
// Serialize to JSON for storage
$this->object->schedule = $this->serializeScheduleConditions($scheduleData);
$this->object->save();
}
return parent::postProcess();
}
```
### 5. Evaluate Schedule at Runtime
```php
// Check if current time matches the schedule
$scheduleData = $this->parseScheduleConditions($object->schedule);
if ($this->evaluateScheduleConditions($scheduleData)) {
// Schedule allows - item is active
} else {
// Schedule blocks - item is inactive
}
// Or check against a specific timestamp
$futureTimestamp = strtotime('+1 day');
$willBeActive = $this->evaluateScheduleConditions($scheduleData, $futureTimestamp);
```
## Configuration Options
```php
$this->renderScheduleConditionsHtml([
// Unique ID for the widget
'id' => 'schedule-conditions',
// Form field name prefix
'name_prefix' => 'schedule_',
// Header text
'title' => 'Schedule',
'subtitle' => 'Define when this should be active',
// Which sections to show
'show_datetime_range' => true, // Start/end datetime pickers
'show_weekly_schedule' => true, // Per-day time range sliders
'show_holiday_exclusions' => true, // Holiday exclusion controls
], $savedData);
```
## Data Format
The schedule data is stored as JSON:
```json
{
"enabled": true,
"datetime_start": "2024-12-24 08:00:00",
"datetime_end": "2024-12-24 18:00:00",
"weekly_schedule": {
"0": {"enabled": false, "start": 0, "end": 1440},
"1": {"enabled": true, "start": 540, "end": 1020},
"2": {"enabled": true, "start": 540, "end": 1020},
"3": {"enabled": true, "start": 540, "end": 1020},
"4": {"enabled": true, "start": 540, "end": 1020},
"5": {"enabled": true, "start": 540, "end": 1020},
"6": {"enabled": false, "start": 0, "end": 1440}
},
"exclude_holidays": true,
"holiday_countries": [1, 14, 17]
}
```
### Field Descriptions
- **enabled**: Whether scheduling is active (false = always active)
- **datetime_start/end**: ISO datetime strings for absolute date range
- **weekly_schedule**: Per-day configuration (0=Sunday, 1=Monday, etc.)
- **enabled**: Whether this day is active
- **start/end**: Minutes from midnight (0-1440, e.g., 540 = 09:00)
- **exclude_holidays**: Whether to exclude public holidays
- **holiday_countries**: Country IDs to check for holidays
## Public Holiday Integration
To enable holiday exclusions, implement the `HolidayProviderInterface`:
```php
<?php
use MyPrestaRocks\TargetConditions\HolidayProviderInterface;
class MyHolidayProvider implements HolidayProviderInterface
{
/**
* Check if a date is a holiday for any of the given countries
*/
public static function isHoliday($date, array $countryIds)
{
// Your implementation
return Db::getInstance()->getValue('
SELECT 1 FROM ps_holidays
WHERE holiday_date = "' . pSQL($date) . '"
AND id_country IN (' . implode(',', array_map('intval', $countryIds)) . ')
') ? true : false;
}
/**
* Get list of country IDs that have holidays configured
*/
public static function getCountriesWithHolidays()
{
return array_column(
Db::getInstance()->executeS('SELECT DISTINCT id_country FROM ps_holidays'),
'id_country'
);
}
/**
* Get holidays within a date range
*/
public static function getHolidaysInRange(array $countryIds, $startDate, $endDate)
{
return Db::getInstance()->executeS('
SELECT holiday_date, display_name, country_iso
FROM ps_holidays h
JOIN ps_country c ON c.id_country = h.id_country
WHERE h.id_country IN (' . implode(',', array_map('intval', $countryIds)) . ')
AND holiday_date BETWEEN "' . pSQL($startDate) . '" AND "' . pSQL($endDate) . '"
');
}
}
```
Then override the provider method in your controller:
```php
protected function getScheduleHolidayProvider()
{
return 'MyHolidayProvider';
}
```
## Helper Methods
```php
// Get human-readable description of schedule
$description = $this->getScheduleDescription($scheduleData);
// Returns: "Dec 24 08:00 to Dec 24 18:00 • Weekdays only • Excl. holidays (3 countries)"
// Format minutes to time string
$time = $this->formatMinutesToTime(540); // Returns "09:00"
// Parse time string to minutes
$minutes = $this->parseTimeToMinutes("09:00"); // Returns 540
```
---
# Assets
Both traits share a common CSS file with trait-specific styling:
| File | Purpose |
|------|---------|
| `assets/css/admin/condition-traits.css` | Shared styling for both traits |
| `assets/js/admin/target-conditions.js` | TargetConditions widget functionality |
| `assets/js/admin/schedule-conditions.js` | ScheduleConditions widget functionality |
## CSS Classes Reference
### TargetConditions
```css
.condition-trait { } /* Main wrapper */
.condition-group { } /* Individual group container */
.condition-group-header { } /* Group header with label */
.condition-group-items { } /* Items container */
.condition-item { } /* Individual selectable item */
.condition-item.selected { } /* Selected state */
.group-modifiers { } /* Limit/sort controls container */
.condition-trait-toggle { } /* All/restricted toggle badge */
```
### ScheduleConditions
```css
.schedule-conditions { } /* Main wrapper */
.schedule-section { } /* Section container */
.schedule-datetime-range { } /* Datetime range section */
.schedule-weekly { } /* Weekly schedule section */
.schedule-holidays { } /* Holiday exclusions section */
.day-timeline { } /* Individual day row */
.timeline-track { } /* Time range slider track */
.timeline-range { } /* Active time range bar */
.timeline-handle { } /* Draggable handles */
```
---
# Requirements
- PrestaShop 1.7.x or 8.x
- PHP 7.1+
# License
MIT License

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,588 @@
/**
* Target Conditions List Preview Styles
*/
/* Trigger in list */
.target-preview-trigger {
cursor: pointer;
display: inline-flex;
gap: 4px;
transition: opacity 0.15s;
}
.target-preview-trigger:hover {
opacity: 0.8;
}
.target-preview-trigger .badge {
cursor: pointer;
}
/* Backdrop */
.target-list-preview-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10500;
background: transparent;
}
/* Popover container */
.target-list-preview-popover {
position: fixed;
z-index: 10501;
width: 380px;
height: 500px;
max-height: calc(100vh - 100px);
background: #fff;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Header */
.target-list-preview-popover .preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
flex-shrink: 0;
}
.target-list-preview-popover .preview-title {
font-weight: 600;
font-size: 13px;
color: #333;
}
.target-list-preview-popover .preview-close {
background: none;
border: none;
padding: 4px 8px;
cursor: pointer;
color: #666;
font-size: 14px;
line-height: 1;
border-radius: 4px;
transition: all 0.15s;
}
.target-list-preview-popover .preview-close:hover {
background: #e9ecef;
color: #333;
}
/* Tabs */
.target-list-preview-popover .preview-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 8px 12px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
flex-shrink: 0;
max-height: 120px;
overflow-y: auto;
}
.target-list-preview-popover .preview-tab {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 4px 8px;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
color: #666;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.target-list-preview-popover .preview-tab:hover {
background: #f8f9fa;
border-color: #adb5bd;
}
.target-list-preview-popover .preview-tab.active {
background: #337ab7;
border-color: #337ab7;
color: #fff;
}
.target-list-preview-popover .preview-tab i {
font-size: 10px;
}
/* Filter bar */
.target-list-preview-popover .preview-filter {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #fff;
border-bottom: 1px solid #e9ecef;
flex-shrink: 0;
}
.target-list-preview-popover .preview-filter i {
color: #adb5bd;
font-size: 12px;
}
.target-list-preview-popover .preview-filter-input {
flex: 1;
border: none;
outline: none;
font-size: 13px;
color: #333;
background: transparent;
}
.target-list-preview-popover .preview-filter-input::placeholder {
color: #adb5bd;
}
/* Content containers */
.target-list-preview-popover .preview-contents {
flex: 1 1 0;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
}
.target-list-preview-popover .preview-content {
display: none;
flex-direction: column;
flex: 1 1 0;
min-height: 0;
overflow: hidden;
}
.target-list-preview-popover .preview-content.active {
display: flex;
}
/* Loading state */
.target-list-preview-popover .preview-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 40px 20px;
color: #666;
font-size: 13px;
}
.target-list-preview-popover .preview-loading i {
font-size: 16px;
color: #337ab7;
}
/* Empty state */
.target-list-preview-popover .preview-empty,
.target-list-preview-popover .preview-empty-inline {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 40px 20px;
color: #999;
font-size: 13px;
}
.target-list-preview-popover .preview-empty i,
.target-list-preview-popover .preview-empty-inline i {
font-size: 16px;
}
/* Error state */
.target-list-preview-popover .preview-error,
.target-list-preview-popover .preview-error-inline {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 40px 20px;
color: #dc3545;
font-size: 13px;
}
.target-list-preview-popover .preview-error i,
.target-list-preview-popover .preview-error-inline i {
font-size: 16px;
}
/* Items list */
.target-list-preview-popover .preview-items {
padding: 8px;
flex: 1 1 0;
overflow-y: auto;
min-height: 0;
}
.target-list-preview-popover .preview-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 6px;
transition: background 0.15s;
}
.target-list-preview-popover .preview-item:hover {
background: #f8f9fa;
}
.target-list-preview-popover .preview-item-image {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
background: #f8f9fa;
flex-shrink: 0;
}
.target-list-preview-popover .preview-item-no-image {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #f1f3f5;
border-radius: 4px;
color: #adb5bd;
font-size: 16px;
flex-shrink: 0;
}
.target-list-preview-popover .preview-item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.target-list-preview-popover .preview-item-name {
font-size: 13px;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.target-list-preview-popover .preview-item-ref {
font-size: 11px;
color: #999;
}
.target-list-preview-popover .preview-item-price {
font-size: 12px;
font-weight: 600;
color: #28a745;
}
/* Footer with load more */
.target-list-preview-popover .preview-footer {
padding: 8px 12px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
flex-shrink: 0;
}
.target-list-preview-popover .load-more-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.target-list-preview-popover .load-more-label {
font-size: 12px;
color: #666;
}
.target-list-preview-popover .load-more-select {
padding: 4px 6px;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
color: #333;
cursor: pointer;
min-width: 70px;
}
.target-list-preview-popover .load-more-select:hover {
border-color: #337ab7;
}
.target-list-preview-popover .load-more-select:focus {
outline: none;
border-color: #337ab7;
box-shadow: 0 0 0 2px rgba(51, 122, 183, 0.2);
}
.target-list-preview-popover .load-more-of {
font-size: 12px;
color: #666;
}
.target-list-preview-popover .load-more-of .remaining-count {
font-weight: 600;
color: #333;
}
.target-list-preview-popover .btn-load-more {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
background: #337ab7;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
transition: background 0.15s;
}
.target-list-preview-popover .btn-load-more:hover {
background: #286090;
}
.target-list-preview-popover .btn-load-more i {
font-size: 11px;
}
.target-list-preview-popover .loading-text,
.target-list-preview-popover .error-text {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #666;
}
.target-list-preview-popover .error-text {
color: #dc3545;
}
.target-list-preview-popover .loading-text i {
color: #337ab7;
}
/* Rules summary */
.target-list-preview-popover .preview-rules {
padding: 8px 12px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
flex-shrink: 0;
max-height: 140px;
overflow-y: auto;
}
.target-list-preview-popover .preview-rule-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
padding: 6px 0;
border-bottom: 1px dashed #dee2e6;
}
.target-list-preview-popover .preview-rule-group:last-child {
border-bottom: none;
padding-bottom: 0;
}
.target-list-preview-popover .preview-rule-group:first-child {
padding-top: 0;
}
.target-list-preview-popover .preview-group-label {
font-size: 10px;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
margin-right: 4px;
flex-shrink: 0;
}
.target-list-preview-popover .preview-rule {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 500;
cursor: help;
transition: transform 0.1s, box-shadow 0.1s;
}
.target-list-preview-popover .preview-rule:hover {
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
}
.target-list-preview-popover .preview-rule.include {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.target-list-preview-popover .preview-rule.exclude {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.target-list-preview-popover .preview-rule .rule-icon {
font-size: 8px;
}
.target-list-preview-popover .preview-rule .rule-icon i {
vertical-align: middle;
}
.target-list-preview-popover .preview-rule .rule-text {
white-space: nowrap;
}
/* Rule detail popup - appended to body */
.rule-detail-popup {
position: fixed;
min-width: 200px;
max-width: 300px;
max-height: 200px;
overflow-y: auto;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10600;
padding: 8px;
}
.rule-detail-loading,
.rule-detail-error,
.rule-detail-info {
font-size: 12px;
color: #666;
text-align: center;
padding: 8px;
}
.rule-detail-info strong {
display: block;
margin-bottom: 4px;
}
.rule-detail-error {
color: #dc3545;
}
.rule-detail-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.rule-detail-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px;
border-radius: 4px;
font-size: 12px;
color: #333;
}
.rule-detail-item:hover {
background: #f8f9fa;
}
.rule-detail-img {
width: 24px;
height: 24px;
object-fit: cover;
border-radius: 3px;
flex-shrink: 0;
}
.rule-detail-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: #f1f3f5;
border-radius: 3px;
color: #6c757d;
font-size: 12px;
flex-shrink: 0;
}
/* Drag and Resize */
.target-list-preview-popover .preview-header {
cursor: move;
user-select: none;
}
.target-list-preview-popover .preview-header .preview-close {
cursor: pointer;
}
.target-list-preview-popover .popover-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, #adb5bd 50%, #adb5bd 60%, transparent 60%, transparent 70%, #adb5bd 70%, #adb5bd 80%, transparent 80%);
border-radius: 0 0 8px 0;
opacity: 0.6;
transition: opacity 0.15s;
}
.target-list-preview-popover .popover-resize-handle:hover {
opacity: 1;
}
.target-list-preview-popover.dragging {
cursor: move;
opacity: 0.95;
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2), 0 5px 15px rgba(0, 0, 0, 0.15);
}
.target-list-preview-popover.resizing {
cursor: se-resize;
opacity: 0.95;
}
.target-list-preview-popover.dragging *,
.target-list-preview-popover.resizing * {
user-select: none;
pointer-events: none;
}
.target-list-preview-popover.dragging .preview-header,
.target-list-preview-popover.resizing .popover-resize-handle {
pointer-events: auto;
}

File diff suppressed because it is too large Load Diff

184
assets/css/admin/mpr-modal.css Executable file
View File

@@ -0,0 +1,184 @@
/**
* MPR Search Revolution - Universal Modal Styles
* All admin modals use .mpr-sr-admin-modal class
* Works on both Symfony pages (.show) and Legacy pages (.in)
*/
.mpr-sr-admin-modal.show,
.mpr-sr-admin-modal.in {
display: flex !important;
align-items: center;
justify-content: center;
}
.mpr-sr-admin-modal.show .modal-dialog,
.mpr-sr-admin-modal.in .modal-dialog {
transform: none !important;
top: auto !important;
margin: 0 auto !important;
}
.mpr-sr-admin-modal .modal-dialog {
max-width: 480px;
}
.mpr-sr-admin-modal .modal-content {
padding: 1.25rem;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.mpr-sr-admin-modal .modal-header,
.mpr-sr-admin-modal .modal-body,
.mpr-sr-admin-modal .modal-footer {
padding: 0;
}
.mpr-sr-admin-modal .modal-header {
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
border: none;
}
.mpr-sr-admin-modal .modal-header .modal-title {
font-size: 1.125rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.mpr-sr-admin-modal .modal-header .modal-title i {
margin-right: 8px;
}
.mpr-sr-admin-modal .modal-header .close,
.mpr-sr-admin-modal .modal-header .mpr-close-modal {
display: flex;
align-items: center;
justify-content: center;
margin: 0;
margin-left: auto;
padding: 5px;
background: transparent;
border: none;
cursor: pointer;
transition: opacity 0.2s;
line-height: 1;
opacity: 0.7;
}
.mpr-sr-admin-modal .modal-header .close:hover,
.mpr-sr-admin-modal .modal-header .mpr-close-modal:hover {
opacity: 1;
}
.mpr-sr-admin-modal .modal-body {
text-align: center;
}
.mpr-sr-admin-modal .modal-body .body-title {
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 0.25rem;
}
.mpr-sr-admin-modal .modal-body .modal-amount {
font-size: 2.5rem;
font-weight: 600;
margin-bottom: 1.25rem;
color: #dc2626;
}
.mpr-sr-admin-modal .modal-body .tables-table {
margin-bottom: 1rem;
font-size: 0.875rem;
text-align: left;
}
.mpr-sr-admin-modal .modal-body .tables-table thead th {
background: #f8fafc;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.025em;
color: #64748b;
padding: 0.5rem 0.75rem;
}
.mpr-sr-admin-modal .modal-body .tables-table tbody td {
padding: 0.5rem 0.75rem;
vertical-align: middle;
border-color: #f1f5f9;
}
.mpr-sr-admin-modal .modal-footer {
display: flex;
padding-top: 1rem;
gap: 0.75rem;
border: none;
}
.mpr-sr-admin-modal .modal-footer .btn {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
font-weight: 500;
border-radius: 6px;
position: relative;
z-index: 1;
cursor: pointer;
}
.mpr-sr-admin-modal .modal-footer .btn i {
margin-right: 5px;
}
.mpr-sr-admin-modal .modal-footer .cancel-btn,
.mpr-sr-admin-modal .modal-footer .btn-secondary {
background: #f1f5f9;
border: 1px solid #e2e8f0;
color: #64748b;
}
.mpr-sr-admin-modal .modal-footer .cancel-btn:hover,
.mpr-sr-admin-modal .modal-footer .btn-secondary:hover {
background: #e2e8f0;
color: #475569;
}
.mpr-sr-admin-modal .modal-footer .confirm-btn,
.mpr-sr-admin-modal .modal-footer .btn-primary {
border: none;
color: #fff;
background: linear-gradient(135deg, #337ab7 0%, #286090 100%);
}
.mpr-sr-admin-modal .modal-footer .confirm-btn:hover:not(:disabled),
.mpr-sr-admin-modal .modal-footer .btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
background: linear-gradient(135deg, #286090 0%, #204d74 100%);
}
.mpr-sr-admin-modal .modal-footer .confirm-btn:disabled,
.mpr-sr-admin-modal .modal-footer .btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.mpr-sr-admin-modal .modal-footer .btn-danger {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
border: none;
color: #fff;
}
.mpr-sr-admin-modal .modal-footer .btn-danger:hover:not(:disabled) {
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
transform: translateY(-1px);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

515
assets/js/admin/modal.js Executable file
View File

@@ -0,0 +1,515 @@
/**
* MPR Express Checkout - Standardized Modal Helper
*
* Provides a consistent API for managing modals across the module.
*
* Usage:
* const modal = new MPRModal('my-modal-id');
* modal.show();
* modal.setHeader('success', 'icon-check', 'Operation Complete');
* modal.setBody('<p>Content here</p>');
* modal.setFooter([
* { type: 'cancel', label: 'Close' },
* { type: 'primary', label: 'Continue', icon: 'arrow-right', onClick: () => {} }
* ]);
*/
class MPRModal {
/**
* @param {string} modalId - The modal element ID (without #)
* @param {Object} options - Configuration options
* @param {Function} options.onShow - Callback when modal is shown
* @param {Function} options.onHide - Callback when modal is hidden
* @param {Function} options.onCancel - Callback when cancel/close is clicked
*/
constructor(modalId, options = {}) {
this.modalId = modalId;
this.$modal = $(`#${modalId}`);
this.$header = this.$modal.find('.mpr-modal-header');
this.$title = this.$modal.find('.mpr-modal-title');
this.$titleText = this.$modal.find('.mpr-modal-title-text');
this.$titleIcon = this.$modal.find('.mpr-modal-icon');
this.$body = this.$modal.find('.mpr-modal-body');
this.$footer = this.$modal.find('.mpr-modal-footer');
this.options = options;
this.currentView = null;
this._bindEvents();
}
/**
* Bind modal events
*/
_bindEvents() {
this.$modal.on('shown.bs.modal', () => {
if (typeof this.options.onShow === 'function') {
this.options.onShow();
}
});
this.$modal.on('hidden.bs.modal', () => {
if (typeof this.options.onHide === 'function') {
this.options.onHide();
}
});
this.$modal.on('click', '[data-dismiss="modal"]', () => {
if (typeof this.options.onCancel === 'function') {
this.options.onCancel();
}
});
}
/**
* Show the modal
* @param {Object} options - Bootstrap modal options
*/
show(options = {}) {
if (this.$modal.length === 0) {
console.error('[MPRModal] Modal element not found');
return;
}
this.$modal.modal({
backdrop: options.static ? 'static' : true,
keyboard: !options.static,
...options
});
}
/**
* Hide the modal
*/
hide() {
this.$modal.modal('hide');
}
/**
* Set header appearance
* @param {string} type - Header type: default, primary, success, warning, danger, dark
* @param {string} icon - Icon class without prefix (e.g., 'shield' for icon-shield)
* @param {string} title - Title text
*/
setHeader(type, icon, title) {
// Remove all header type classes
this.$header.removeClass(
'mpr-modal-header-default mpr-modal-header-primary mpr-modal-header-success ' +
'mpr-modal-header-warning mpr-modal-header-danger mpr-modal-header-dark'
);
this.$header.addClass(`mpr-modal-header-${type}`);
if (icon) {
if (this.$titleIcon.length) {
this.$titleIcon.attr('class', `mpr-icon icon-${icon} mpr-modal-icon`);
} else {
this.$title.prepend(`<i class="mpr-icon icon-${icon} mpr-modal-icon"></i>`);
this.$titleIcon = this.$modal.find('.mpr-modal-icon');
}
}
if (title !== undefined) {
this.$titleText.text(title);
}
}
/**
* Set only the header type/color
* @param {string} type - Header type: default, primary, success, warning, danger, dark
*/
setHeaderType(type) {
this.$header.removeClass(
'mpr-modal-header-default mpr-modal-header-primary mpr-modal-header-success ' +
'mpr-modal-header-warning mpr-modal-header-danger mpr-modal-header-dark'
);
this.$header.addClass(`mpr-modal-header-${type}`);
}
/**
* Set the header title
* @param {string} title - Title text
*/
setTitle(title) {
this.$titleText.text(title);
}
/**
* Set the header icon
* @param {string} icon - Icon class without prefix
*/
setIcon(icon) {
if (this.$titleIcon.length) {
this.$titleIcon.attr('class', `icon-${icon} mpr-modal-icon`);
}
}
/**
* Set modal size
* @param {string} size - Size: sm, md, lg, xl, fullwidth
*/
setSize(size) {
const $dialog = this.$modal.find('.modal-dialog');
$dialog.removeClass('modal-sm modal-lg modal-xl modal-fullwidth');
if (size === 'sm') {
$dialog.addClass('modal-sm');
} else if (size === 'lg') {
$dialog.addClass('modal-lg');
} else if (size === 'xl') {
$dialog.addClass('modal-xl');
} else if (size === 'fullwidth') {
$dialog.addClass('modal-fullwidth');
}
}
/**
* Set body content
* @param {string} html - HTML content for the body
*/
setBody(html) {
this.$body.html(html);
}
/**
* Append content to body
* @param {string} html - HTML content to append
*/
appendBody(html) {
this.$body.append(html);
}
/**
* Set footer buttons
* @param {Array} buttons - Array of button configurations
* Each button: { type, label, icon, id, onClick, disabled, className, size }
* type: 'cancel', 'primary', 'success', 'warning', 'danger', 'default', 'dark',
* 'outline-primary', 'outline-danger', 'ghost'
* size: 'sm', 'lg' (optional)
*/
setFooter(buttons) {
this.$footer.empty();
buttons.forEach(btn => {
const btnType = btn.type === 'cancel' ? 'default' : btn.type;
let btnClass = `mpr-btn mpr-btn-${btnType}`;
if (btn.size) {
btnClass += ` mpr-btn-${btn.size}`;
}
if (btn.className) {
btnClass += ` ${btn.className}`;
}
const $btn = $('<button>', {
type: 'button',
class: btnClass,
id: btn.id || undefined,
disabled: btn.disabled || false
});
if (btn.type === 'cancel' || btn.dismiss) {
$btn.attr('data-dismiss', 'modal');
}
// Add icon - cancel buttons get 'close' icon by default if no icon specified
const iconName = btn.icon || (btn.type === 'cancel' ? 'close' : null);
if (iconName) {
$btn.append(`<i class="mpr-icon icon-${iconName}"></i> `);
}
// Support HTML in labels (for inline icons) or plain text
if (btn.html) {
$btn.append(btn.label);
} else {
$btn.append(document.createTextNode(btn.label));
}
if (typeof btn.onClick === 'function') {
$btn.on('click', btn.onClick);
}
this.$footer.append($btn);
});
}
/**
* Set button loading state
* @param {string} buttonId - Button ID
* @param {boolean} loading - Loading state
*/
setButtonLoading(buttonId, loading) {
const $btn = $(`#${buttonId}`);
if (loading) {
$btn.addClass('mpr-btn-loading').prop('disabled', true);
} else {
$btn.removeClass('mpr-btn-loading').prop('disabled', false);
}
}
/**
* Show footer
*/
showFooter() {
this.$footer.removeClass('hidden');
}
/**
* Hide footer
*/
hideFooter() {
this.$footer.addClass('hidden');
}
/**
* Enable/disable a footer button by ID
* @param {string} buttonId - Button ID
* @param {boolean} enabled - Enable or disable
*/
setButtonEnabled(buttonId, enabled) {
$(`#${buttonId}`).prop('disabled', !enabled);
}
/**
* Update button label
* @param {string} buttonId - Button ID
* @param {string} label - New label
* @param {string} icon - Optional new icon
*/
setButtonLabel(buttonId, label, icon = null) {
const $btn = $(`#${buttonId}`);
$btn.empty();
if (icon) {
$btn.append(`<i class="mpr-icon icon-${icon}"></i> `);
}
$btn.append(document.createTextNode(label));
}
/**
* Switch between views (for multi-step modals)
* Views should have class 'mpr-modal-view' and a data-view attribute
* @param {string} viewName - The view to show
*/
showView(viewName) {
this.$body.find('.mpr-modal-view').removeClass('active');
this.$body.find(`[data-view="${viewName}"]`).addClass('active');
this.currentView = viewName;
}
/**
* Get current view name
* @returns {string|null}
*/
getCurrentView() {
return this.currentView;
}
/**
* Create and show a simple confirmation modal
* @param {Object} config - Configuration
* @param {string} config.type - Header type
* @param {string} config.icon - Header icon
* @param {string} config.title - Title
* @param {string} config.message - Body message (can be HTML)
* @param {string} config.confirmLabel - Confirm button label
* @param {string} config.confirmType - Confirm button type (primary, danger, etc.)
* @param {string} config.cancelLabel - Cancel button label
* @param {Function} config.onConfirm - Confirm callback
* @param {Function} config.onCancel - Cancel callback
*/
confirm(config) {
this.setHeader(
config.type || 'primary',
config.icon || 'question',
config.title || 'Confirm'
);
this.setBody(`
<div class="mpr-modal-center">
<p>${config.message}</p>
</div>
`);
this.setFooter([
{
type: 'cancel',
label: config.cancelLabel || 'Cancel',
onClick: config.onCancel
},
{
type: config.confirmType || 'primary',
label: config.confirmLabel || 'Confirm',
icon: config.confirmIcon,
onClick: () => {
if (typeof config.onConfirm === 'function') {
config.onConfirm();
}
if (config.autoClose !== false) {
this.hide();
}
}
}
]);
this.show({ static: config.static || false });
}
/**
* Show a progress state
* @param {Object} config - Configuration
* @param {string} config.title - Progress title
* @param {string} config.subtitle - Progress subtitle
* @param {number} config.percent - Initial percentage (0-100)
*/
showProgress(config = {}) {
const title = config.title || 'Processing...';
const subtitle = config.subtitle || 'Please wait';
const percent = config.percent || 0;
this.setBody(`
<div class="mpr-modal-progress">
<i class="icon-refresh mpr-modal-progress-icon"></i>
<div class="mpr-modal-progress-title">${title}</div>
<div class="mpr-modal-progress-subtitle">${subtitle}</div>
<div class="mpr-modal-progress-bar-container">
<div class="mpr-modal-progress-bar" style="width: ${percent}%"></div>
</div>
<div class="mpr-modal-progress-percent">${percent}%</div>
<div class="mpr-modal-progress-current"></div>
</div>
`);
this.hideFooter();
this.setHeaderType('primary');
}
/**
* Update progress bar
* @param {number} percent - Percentage (0-100)
* @param {string} currentItem - Current item being processed
*/
updateProgress(percent, currentItem = '') {
this.$body.find('.mpr-modal-progress-bar').css('width', `${percent}%`);
this.$body.find('.mpr-modal-progress-percent').text(`${Math.round(percent)}%`);
if (currentItem) {
this.$body.find('.mpr-modal-progress-current').text(currentItem);
}
}
/**
* Show a result state
* @param {Object} config - Configuration
* @param {string} config.type - Result type: success, warning, danger, info
* @param {string} config.icon - Icon (defaults based on type)
* @param {string} config.title - Result title
* @param {string} config.message - Result message
* @param {string} config.closeLabel - Close button label
* @param {Function} config.onClose - Close callback
*/
showResult(config) {
const iconMap = {
success: 'check-circle',
warning: 'warning',
danger: 'times-circle',
info: 'info-circle'
};
const icon = config.icon || iconMap[config.type] || 'info-circle';
this.setHeaderType(config.type === 'info' ? 'primary' : config.type);
this.setBody(`
<div class="mpr-modal-result">
<i class="icon-${icon} mpr-modal-result-icon result-${config.type}"></i>
<div class="mpr-modal-result-title">${config.title}</div>
<div class="mpr-modal-result-message">${config.message}</div>
</div>
`);
this.setFooter([
{
type: 'primary',
label: config.closeLabel || 'Close',
onClick: () => {
if (typeof config.onClose === 'function') {
config.onClose();
}
this.hide();
}
}
]);
this.showFooter();
}
/**
* Lock modal (prevent closing)
*/
lock() {
this.$modal.data('bs.modal').options.backdrop = 'static';
this.$modal.data('bs.modal').options.keyboard = false;
this.$modal.find('.mpr-modal-close').hide();
}
/**
* Unlock modal (allow closing)
*/
unlock() {
this.$modal.data('bs.modal').options.backdrop = true;
this.$modal.data('bs.modal').options.keyboard = true;
this.$modal.find('.mpr-modal-close').show();
}
/**
* Destroy the modal instance
*/
destroy() {
this.$modal.modal('dispose');
this.$modal.off();
}
}
/**
* Factory function to create modals dynamically
* Creates the modal HTML and appends it to the body
*
* @param {Object} config - Modal configuration
* @param {string} config.id - Modal ID
* @param {string} config.size - Modal size: sm, md, lg, xl
* @param {boolean} config.static - Static backdrop
* @returns {MPRModal}
*/
MPRModal.create = function(config) {
const id = config.id || 'mpr-modal-' + Date.now();
const sizeClass = config.size === 'sm' ? 'modal-sm' :
config.size === 'lg' ? 'modal-lg' :
config.size === 'xl' ? 'modal-xl' :
config.size === 'fullwidth' ? 'modal-fullwidth' : '';
const html = `
<div class="modal fade mpr-modal" id="${id}" tabindex="-1" role="dialog"
${config.static ? 'data-backdrop="static" data-keyboard="false"' : ''}>
<div class="modal-dialog ${sizeClass}" role="document">
<div class="modal-content">
<div class="modal-header mpr-modal-header mpr-modal-header-primary">
<h5 class="modal-title mpr-modal-title">
<i class="mpr-modal-icon"></i>
<span class="mpr-modal-title-text"></span>
</h5>
<button type="button" class="close mpr-modal-close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body mpr-modal-body"></div>
<div class="modal-footer mpr-modal-footer"></div>
</div>
</div>
</div>
`;
$('body').append(html);
return new MPRModal(id, config);
};
// Export for module systems if available
if (typeof module !== 'undefined' && module.exports) {
module.exports = MPRModal;
}

File diff suppressed because it is too large Load Diff

27
composer.json Executable file
View File

@@ -0,0 +1,27 @@
{
"name": "myprestarocks/prestashop-entity-selector",
"description": "Universal entity selection widget for PrestaShop admin controllers - select products, categories, manufacturers with include/exclude logic",
"keywords": ["prestashop", "admin", "controller", "traits", "entity", "selector", "products", "categories"],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "mypresta.rocks",
"email": "info@mypresta.rocks"
}
],
"require": {
"php": ">=7.1"
},
"autoload": {
"psr-4": {
"MyPrestaRocks\\EntitySelector\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-main": "1.0.x-dev"
}
},
"minimum-stability": "stable"
}

944
package-lock.json generated Executable file
View File

@@ -0,0 +1,944 @@
{
"name": "prestashop-target-conditions",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prestashop-target-conditions",
"version": "1.0.0",
"devDependencies": {
"tailwindcss": "^3.4.1"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"bin": {
"jiti": "bin/jiti.js"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-import": {
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
"resolve": "^1.1.7"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-js": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-load-config": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"lilconfig": "^3.1.1"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"jiti": ">=1.21.0",
"postcss": ">=8.0.9",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
},
"postcss": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/postcss-nested": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"postcss-selector-parser": "^6.1.1"
},
"engines": {
"node": ">=12.0"
},
"peerDependencies": {
"postcss": "^8.2.14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"dependencies": {
"pify": "^2.3.0"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"dev": true,
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
"lines-and-columns": "^1.1.6",
"mz": "^2.7.0",
"pirates": "^4.0.1",
"tinyglobby": "^0.2.11",
"ts-interface-checker": "^0.1.9"
},
"bin": {
"sucrase": "bin/sucrase",
"sucrase-node": "bin/sucrase-node"
},
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwindcss": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
"dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.6.0",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.21.7",
"lilconfig": "^3.1.3",
"micromatch": "^4.0.8",
"normalize-path": "^3.0.0",
"object-hash": "^3.0.0",
"picocolors": "^1.1.1",
"postcss": "^8.4.47",
"postcss-import": "^15.1.0",
"postcss-js": "^4.0.1",
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
"postcss-nested": "^6.2.0",
"postcss-selector-parser": "^6.1.2",
"resolve": "^1.22.8",
"sucrase": "^3.35.0"
},
"bin": {
"tailwind": "lib/cli.js",
"tailwindcss": "lib/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"dependencies": {
"any-promise": "^1.0.0"
}
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
}
}
}

12
package.json Executable file
View File

@@ -0,0 +1,12 @@
{
"name": "prestashop-target-conditions",
"version": "1.0.0",
"description": "Target conditions widget for PrestaShop modules",
"scripts": {
"build:css": "npx tailwindcss -i ./assets/css/admin/tailwind-input.css -o ./assets/css/admin/tailwind-output.css --minify",
"watch:css": "npx tailwindcss -i ./assets/css/admin/tailwind-input.css -o ./assets/css/admin/tailwind-output.css --watch"
},
"devDependencies": {
"tailwindcss": "^3.4.1"
}
}

13490
src/EntitySelector.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
<?php
/**
* HolidayProviderInterface
*
* Interface for holiday functionality in ScheduleConditions.
* Implement this interface in your module to enable public holiday exclusions.
*
* @author mypresta.rocks <info@mypresta.rocks>
* @copyright Copyright (c) mypresta.rocks
* @license MIT
*/
namespace MyPrestaRocks\EntitySelector;
if (!defined('_PS_VERSION_')) {
exit;
}
interface HolidayProviderInterface
{
/**
* Check if a specific date is a public holiday for any of the given countries
*
* @param string $date Date in Y-m-d format
* @param array $countryIds Array of country IDs to check
* @return bool True if date is a holiday for any of the countries
*/
public static function isHoliday($date, array $countryIds);
/**
* Get list of country IDs that have holidays configured
*
* @return array Array of country IDs
*/
public static function getCountriesWithHolidays();
/**
* Get holidays within a date range for specific countries
*
* @param array $countryIds Array of country IDs
* @param string $startDate Start date in Y-m-d format
* @param string $endDate End date in Y-m-d format
* @return array Array of holiday data with keys: holiday_date, display_name, country_iso
*/
public static function getHolidaysInRange(array $countryIds, $startDate, $endDate);
}

1193
src/ScheduleConditions.php Executable file

File diff suppressed because it is too large Load Diff

18
tailwind.config.js Executable file
View File

@@ -0,0 +1,18 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
prefix: 'tw-',
content: [
'./src/**/*.php',
'./assets/js/admin/**/*.js',
// Also scan consuming modules' templates
'../../../views/templates/admin/**/*.tpl',
'../../../controllers/admin/**/*.php',
],
theme: {
extend: {},
},
plugins: [],
corePlugins: {
preflight: false,
},
}