diff --git a/admin/AGENTS.md b/admin/AGENTS.md
index 103af38e..899cfd9f 100644
--- a/admin/AGENTS.md
+++ b/admin/AGENTS.md
@@ -194,12 +194,16 @@ When adding a new entity, build the files in this order, matching the existing f
- Tables use `public static function table(Table $table): Table` with `->columns([])`, `->filters([])`, `->recordActions([...])`, `->toolbarActions([...])`.
- Actions come from the `Filament\Actions\` namespace (`EditAction`, `CreateAction`, `DeleteAction`, `BulkActionGroup`, `DeleteBulkAction`).
- Import individual components (`Filament\Forms\Components\TextInput`, `Filament\Tables\Columns\TextColumn`), not the parent `Forms`/`Tables` namespaces.
+- For reactive `->options()` or `->live()` closures that receive `Get $get`, import `Filament\Schemas\Components\Utilities\Get` (NOT `Filament\Forms\Get` - that will throw a type error at runtime).
- Page classes set `protected static string $resource = {Name}Resource::class;`. List pages expose `CreateAction::make()` in `getHeaderActions()`. Create and Edit pages redirect with `getRedirectUrl(): string` returning `$this->getResource()::getUrl('index')`.
- Rich text uses `AmidEsfahani\FilamentTinyEditor\TinyEditor`.
- Select fields backed by an enum use `->options(SomeEnum::options())` and `->default(SomeEnum::CASE->value)`.
- Table text columns that can be long (headings, relation labels) use `->limit(30)->wrap()`.
- Enum-backed table columns render via `->getStateUsing(fn ($record) => $record->field->label())` and `->color(fn ($record) => $record->field->color())`.
- Manage many-to-many pivots with a relationship multi-select: `Select::make('products')->relationship('products', 'heading')->multiple()->searchable()->preload()` (see `CouponResource`). No separate resource for pure scoping pivots.
+- Control navigation order within a group with `protected static ?int $navigationSort = 1;` (lower = higher in the list).
+- Add an explanatory subheading to a list page with `protected ?string $subheading = 'Description here.';` on the `ListRecords` page class.
+- Add a tooltip to a form field with `->hintIcon('heroicon-o-information-circle')->hintIconTooltip('Explanation...')`. Use this instead of always-visible `->hint()` when the text is long.
## Models
@@ -218,6 +222,7 @@ When adding a new entity, build the files in this order, matching the existing f
## Migrations
+- **Development rule**: NEVER create a new migration to add a column to a table that already has a migration in this branch. Update the existing `create_*` migration directly to keep history clean. After editing, run `php artisan migrate:fresh` inside the container to re-apply everything from scratch. Only create additive migrations when the table already exists in production.
- Anonymous class style: `return new class extends Migration`.
- Use `$table->foreignIdFor(Model::class)` for foreign keys (add `->nullable()` when optional). Chain `->constrained()->cascadeOnDelete()` / `->nullOnDelete()` / `->restrictOnDelete()` to add the real FK constraint with its delete rule.
- For a second FK to the same table, use a named column: `$table->foreignId('user_creator_id')->nullable()->constrained('users')->nullOnDelete()` (see `coupons`).
@@ -255,5 +260,6 @@ When adding a new entity, build the files in this order, matching the existing f
- The implementation status and priority order live in `IMPLEMENTATION.md`. When an entity is finished or the plan changes, update it.
- The full schema reference is `ShoFlow db doc.md`. Treat it as the source of truth for table columns and relationships.
+- Cache keys that have been identified but not yet implemented are tracked in `CACHE.md`. When adding a model whose data is likely to be cached (products, categories, banners, menus, etc.), check `CACHE.md` and add or update the relevant rows.
- Keep this "ShopFlow Admin Conventions" section updated whenever a new reusable pattern is introduced.
diff --git a/admin/CACHE.md b/admin/CACHE.md
new file mode 100644
index 00000000..adc39e4b
--- /dev/null
+++ b/admin/CACHE.md
@@ -0,0 +1,37 @@
+# ShopFlow Cache Register
+
+Tracks cache keys that have been identified but are not yet implemented.
+When a cache is implemented, move it to the **Implemented** section and record the key, driver, TTL, and where it is invalidated.
+
+Legend: `[ ]` not started, `[x]` implemented.
+
+> **Note:** `variety_counts` on `products` is NOT a cache. It is a denormalized DB column kept in sync by `Variety::booted()` (saved/deleted events calling `syncProductVarietyCount()`). No cache entry needed for it.
+
+---
+
+## Pending (identified, not implemented)
+
+| # | Cache key / pattern | What it caches | Suggested TTL | Invalidated when |
+|---|---------------------|----------------|---------------|-----------------|
+| 1 | `categories.tree` | Full nested category tree | 1 hour | Category saved / deleted |
+| 2 | `banners.{position}` | Published banners for a given position | 30 min | Banner saved / deleted |
+| 3 | `menus.{slug}` | Rendered menu tree for a given slug | 1 hour | Menu or MenuItem saved / deleted |
+| 4 | `attributes.group.{group_id}` | Attributes belonging to a group | 1 hour | Attribute saved / deleted |
+| 5 | `products.slug.{slug}` | Single product record (with images, attributes, brand, category) | 30 min | Product saved / deleted |
+| 6 | `products.category.{category_id}` | Published products list for a category page | 15 min | Product saved / deleted |
+| 7 | `varieties.product.{product_id}` | All varieties for a product (price, inventory, status) | 15 min | Variety saved / deleted |
+
+---
+
+## Implemented
+
+_None yet._
+
+---
+
+## Rules
+
+- Cache keys use dot notation and include any dynamic segment as `{placeholder}`.
+- Tag caches by entity name so bulk-invalidation is possible (e.g. `Cache::tags(['banners'])->flush()`).
+- Invalidation logic goes in the model's `booted()` method or a dedicated observer, never in controllers or resources.
+- Document every new key here before or when implementing it.
diff --git a/admin/IMPLEMENTATION.md b/admin/IMPLEMENTATION.md
index 9b22c0e3..0fc77b1b 100644
--- a/admin/IMPLEMENTATION.md
+++ b/admin/IMPLEMENTATION.md
@@ -24,17 +24,23 @@ Catalog layer and platform basics.
- [x] Discounts (auto-applied price rules per variety)
- [x] Coupons (+ `coupon_product`, `coupon_variety`, `category_coupon` scoping pivots)
- [x] Images (polymorphic, used via uploads - no standalone resource by design)
+- [x] Banners (polymorphic images via `images` table)
- [~] Addresses (model only, no resource yet)
Sample data for manual admin testing lives in `TestSeeder` (`php artisan db:seed --class=TestSeeder`); `DatabaseSeeder` holds only necessary data.
+Cross-cutting improvements landed:
+- Navigation groups reorganized: Catalog / Promotions / Attribute / Content / Address.
+- `CACHE.md` added to track identified-but-not-implemented cache keys.
+
## Phase 0 - Finish current branch (`implement_variety`)
Done. Variety has model, migration, factory, resource (+ pages), tests, and `variety_counts` auto-sync.
- [x] Fix `VarietyResource` table: `product.heading` column (was `product.title` with `->numeric()` on a string)
-- [ ] Variety extensions (`warehouse_id`, `guarantee_name_id`, `attribute_id`, `variety_attribute`, `variety_serials`, `variety_details`) - deferred to Phase 3, they need Warehouses / Guarantees first
-- [ ] Attribute `as_filter` / `required` flags into the product/category flow (open `// todo` in the attribute-group-category migration) - moved to its own branch; it is an attribute/category feature, not variety
+- [x] `attribute_id` FK on varieties - links each variety to one attribute; auto-populates `attribute_value` and `color` from the attribute on save
+- [ ] Variety extensions (`warehouse_id`, `guarantee_name_id`, `variety_serials`, `variety_details`) - deferred to Phase 3, they need Warehouses / Guarantees first
+- [x] Attribute `required` flag enforced in `ProductResource`: if a category has required attribute groups, saving a product without them shows a danger notification and skips the sync
## Phase 1 - Pricing & promotions (recommended next)
@@ -42,12 +48,16 @@ Builds on Products/Varieties; prerequisite for Orders.
- [x] Discounts (auto-applied price rules per variety)
- [x] Coupons (+ `coupon_product`, `coupon_variety`, `category_coupon` scoping pivots)
+- [ ] Cron jobs for Discounts & Coupons:
+ - Auto-expire discounts when `ended_at` is passed (set status or filter in queries)
+ - Auto-expire coupons when `expired_at` is passed
+ - Optionally: reset `sold` / `total_used` counters on a schedule if needed
## Phase 2 - CMS / content (independent quick wins)
Depend mostly on Images only.
-- [ ] Banners
+- [x] Banners
- [ ] Sliders + Slides
- [ ] Menus + Menu Items
- [ ] Pages
diff --git a/admin/ShoFlow db doc.md b/admin/ShoFlow db doc.md
index ac617d4f..9a1cf06b 100644
--- a/admin/ShoFlow db doc.md
+++ b/admin/ShoFlow db doc.md
@@ -94,11 +94,10 @@ Used to store banners.
* `position` specifies the advertisement location, which is an arbitrary name to retrieve the corresponding record from the database.
* `heading` specifies the banner item title or the alt text of the image.
* `url` specifies the item link, which redirects when the image or title is clicked.
-* `image_id1` specifies the item's image.
-* `image_id2` specifies the item's image.
-* `image_id3` specifies the item's image.
* `sort` specifies the item order.
-* `status` specifies the publication and draft status of the banner.
+* `status` stores the publication status of the banner, with values 10 for deleted, 20 for published, and 30 for draft.
+
+Images are attached through the polymorphic `images` table (`imageable_type` / `imageable_id`), so a banner can have one or more images instead of fixed `image_id` columns. The featured image is the one with `is_featured` set to true.
# Brand\_Category
@@ -374,7 +373,7 @@ In short: discounts are automatic, per-variety, condition-based price rules. The
* Stores images.
* `path`: Stores the relative path of the images, the absolute path can be specified using the `static_asset` function relative to the CDN.
-* `imageable_type:` used for polymorphic relation Attributes Brand Categories
+* `imageable_type:` used for polymorphic relation Attributes, Brand, Categories, Products, Banners
* `imageable_id:` used for polymorphic relation
* `created_at`: Specifies the record creation date.
@@ -818,9 +817,10 @@ For storing user permissions and removing role-based permissions.
Contains product variations entered by the seller on the site. This table creates a record depending on the selected variation and price.
-* `product_id`: Indicates which product this variety belongs to. Foreign key to `products`; cascades on product delete.
-* `attribute_value`: Free-text value of the selected variation (e.g., "Red", "8GB"). Currently not linked to the `attributes` table.
-* `color`: Optional color value for the variation.
+* `product_id`: Indicates which product this variety belongs to. Foreign key to `products`; cascades on product delete.
+* `attribute_id`: Links to the `attributes` table - the specific attribute that defines this variety (e.g. "Red", "XL"). Nullable FK; set to null when the attribute is deleted. When set, `attribute_value` and `color` are auto-populated from the linked attribute via model saving event.
+* `attribute_value`: Display label for the variation, auto-populated from `attribute.value` when `attribute_id` is set. Can also be set manually when no attribute is linked.
+* `color`: Hex or name color for the variation, auto-populated from `attribute.color` when `attribute_id` is set.
* `price`
* `sale_price`
* `inventory`: Number of ShopFlow inventory, the default value is 0\.
diff --git a/admin/app/Enums/BannerStatusEnum.php b/admin/app/Enums/BannerStatusEnum.php
new file mode 100644
index 00000000..9eb7dfb0
--- /dev/null
+++ b/admin/app/Enums/BannerStatusEnum.php
@@ -0,0 +1,34 @@
+ 'Deleted',
+ self::PUBLISHED => 'Published',
+ self::DRAFT => 'Draft',
+ };
+ }
+
+ public function color(): string
+ {
+ return match ($this) {
+ self::DELETED => 'danger',
+ self::PUBLISHED => 'success',
+ self::DRAFT => 'warning',
+ };
+ }
+}
diff --git a/admin/app/Filament/Resources/AncestorResource.php b/admin/app/Filament/Resources/AncestorResource.php
index 8e9650b4..375e9ab4 100644
--- a/admin/app/Filament/Resources/AncestorResource.php
+++ b/admin/app/Filament/Resources/AncestorResource.php
@@ -23,6 +23,8 @@ class AncestorResource extends Resource
protected static string | \UnitEnum | null $navigationGroup = 'Attribute';
+ protected static ?int $navigationSort = 1;
+
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Schema $schema): Schema
diff --git a/admin/app/Filament/Resources/AttributeGroupCategoryResource.php b/admin/app/Filament/Resources/AttributeGroupCategoryResource.php
index bfa7a91e..71221c95 100644
--- a/admin/app/Filament/Resources/AttributeGroupCategoryResource.php
+++ b/admin/app/Filament/Resources/AttributeGroupCategoryResource.php
@@ -25,6 +25,8 @@ class AttributeGroupCategoryResource extends Resource
protected static string | \UnitEnum | null $navigationGroup = 'Attribute';
+ protected static ?int $navigationSort = 3;
+
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Schema $schema): Schema
@@ -33,13 +35,21 @@ public static function form(Schema $schema): Schema
->components([
Select::make('attribute_group_id')
->relationship('attributeGroup', 'name')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The attribute group to attach to the category (e.g. "Color", "Size").')
->required(),
Select::make('category_id')
->relationship('category', 'title')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The category this attribute group will be available for.')
->required(),
Toggle::make('as_filter')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When enabled, this group appears as a filter panel on the category\'s product listing page.')
->required(),
Toggle::make('required')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When enabled, admins must select at least one attribute from this group before saving a product in this category.')
->required(),
]);
}
diff --git a/admin/app/Filament/Resources/AttributeGroupCategoryResource/Pages/ListAttributeGroupCategories.php b/admin/app/Filament/Resources/AttributeGroupCategoryResource/Pages/ListAttributeGroupCategories.php
index cef32cfa..f74ed450 100644
--- a/admin/app/Filament/Resources/AttributeGroupCategoryResource/Pages/ListAttributeGroupCategories.php
+++ b/admin/app/Filament/Resources/AttributeGroupCategoryResource/Pages/ListAttributeGroupCategories.php
@@ -12,6 +12,8 @@ class ListAttributeGroupCategories extends ListRecords
{
protected static string $resource = AttributeGroupCategoryResource::class;
+ protected ?string $subheading = 'Links an attribute group to a category. This tells the system which attributes are relevant for products in that category. Use "As Filter" to show the group as a filter panel on the category page, and "Required" to force admins to pick an attribute from this group when creating a product in that category.';
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Filament/Resources/AttributeGroupResource.php b/admin/app/Filament/Resources/AttributeGroupResource.php
index 545d2b71..4c3a4e01 100644
--- a/admin/app/Filament/Resources/AttributeGroupResource.php
+++ b/admin/app/Filament/Resources/AttributeGroupResource.php
@@ -24,6 +24,8 @@ class AttributeGroupResource extends Resource
protected static string | \UnitEnum | null $navigationGroup = 'Attribute';
+ protected static ?int $navigationSort = 2;
+
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Schema $schema): Schema
@@ -32,11 +34,15 @@ public static function form(Schema $schema): Schema
->components([
Select::make('ancestor_id')
->relationship('ancestor', 'name')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Groups attribute groups under a common ancestor (e.g. "Physical", "Technical"). Create ancestors first.')
->required(),
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('label')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The label shown to the customer on the frontend (e.g. "Choose a color"). If empty, the name is used.')
->maxLength(255),
TextInput::make('order')
->numeric(),
diff --git a/admin/app/Filament/Resources/AttributeGroupResource/Pages/ListAttributeGroups.php b/admin/app/Filament/Resources/AttributeGroupResource/Pages/ListAttributeGroups.php
index 3ec1fbea..b4d8271e 100644
--- a/admin/app/Filament/Resources/AttributeGroupResource/Pages/ListAttributeGroups.php
+++ b/admin/app/Filament/Resources/AttributeGroupResource/Pages/ListAttributeGroups.php
@@ -12,6 +12,8 @@ class ListAttributeGroups extends ListRecords
{
protected static string $resource = AttributeGroupResource::class;
+ protected ?string $subheading = 'An attribute group is a set of attributes that share a common property (e.g. "Color" contains Red, Blue, Green). Each group belongs to an ancestor and is linked to categories via Attribute Group Categories. Products then use a group to define their variety differentiator.';
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Filament/Resources/AttributeResource.php b/admin/app/Filament/Resources/AttributeResource.php
index c19dbce4..e35f7578 100644
--- a/admin/app/Filament/Resources/AttributeResource.php
+++ b/admin/app/Filament/Resources/AttributeResource.php
@@ -24,6 +24,8 @@ class AttributeResource extends Resource
protected static string | \UnitEnum | null $navigationGroup = 'Attribute';
+ protected static ?int $navigationSort = 4;
+
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-rectangle-stack';
public static function form(Schema $schema): Schema
@@ -32,11 +34,17 @@ public static function form(Schema $schema): Schema
->components([
Select::make('attribute_group_id')
->relationship('attributeGroup', 'name')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The group this attribute belongs to (e.g. "Color", "Size"). Create attribute groups first.')
->required(),
TextInput::make('value')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The display value of this attribute (e.g. "Red", "XL"). Shown to customers and used as the variety label.')
->required()
->maxLength(255),
TextInput::make('color')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Optional hex or color name (e.g. #ff0000). Auto-copied to the variety\'s color when this attribute is selected.')
->nullable()
->maxLength(31),
]);
diff --git a/admin/app/Filament/Resources/AttributeResource/Pages/ListAttributes.php b/admin/app/Filament/Resources/AttributeResource/Pages/ListAttributes.php
index e146875f..0e568f2d 100644
--- a/admin/app/Filament/Resources/AttributeResource/Pages/ListAttributes.php
+++ b/admin/app/Filament/Resources/AttributeResource/Pages/ListAttributes.php
@@ -12,6 +12,8 @@ class ListAttributes extends ListRecords
{
protected static string $resource = AttributeResource::class;
+ protected ?string $subheading = 'Attributes are the individual values inside an attribute group (e.g. "Red", "Blue", "Green" inside "Color"). Each attribute can have an optional color swatch. When linked to a variety via attribute_id, the variety\'s value and color are auto-filled from the attribute.';
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Filament/Resources/BannerResource.php b/admin/app/Filament/Resources/BannerResource.php
new file mode 100644
index 00000000..818b8a9c
--- /dev/null
+++ b/admin/app/Filament/Resources/BannerResource.php
@@ -0,0 +1,130 @@
+components([
+ TextInput::make('position')
+ ->required()
+ ->maxLength(255),
+ TextInput::make('heading')
+ ->required()
+ ->maxLength(255),
+ TextInput::make('url')
+ ->url()
+ ->maxLength(255),
+ TextInput::make('sort')
+ ->numeric()
+ ->nullable(),
+ Select::make('status')
+ ->required()
+ ->options(BannerStatusEnum::options())
+ ->default(BannerStatusEnum::PUBLISHED->value),
+ Repeater::make('images')
+ ->relationship('images')
+ ->schema([
+ FileUpload::make('path')
+ ->nullable()
+ ->columns(1)
+ ->columnSpanFull(),
+ Toggle::make('is_featured')
+ ->label('Featured Image')
+ ->reactive(),
+ TextInput::make('alt_text')
+ ->label('Alt Text'),
+ ])
+ ->columnSpanFull(),
+ ]);
+ }
+
+ public static function table(Table $table): Table
+ {
+ return $table
+ ->columns([
+ TextColumn::make('position')
+ ->searchable(),
+ TextColumn::make('heading')
+ ->limit(30)
+ ->wrap()
+ ->searchable(),
+ ImageColumn::make('featuredImage.path')
+ ->label('Featured')
+ ->square(),
+ TextColumn::make('url')
+ ->limit(30),
+ TextColumn::make('sort')
+ ->numeric()
+ ->sortable(),
+ TextColumn::make('status')
+ ->getStateUsing(fn (Banner $record): string => $record->status->label())
+ ->color(fn (Banner $record): string => $record->status->color())
+ ->sortable(),
+ TextColumn::make('created_at')
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ TextColumn::make('updated_at')
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ ])
+ ->filters([
+ //
+ ])
+ ->recordActions([
+ EditAction::make(),
+ ])
+ ->toolbarActions([
+ BulkActionGroup::make([
+ DeleteBulkAction::make(),
+ ]),
+ ]);
+ }
+
+ public static function getRelations(): array
+ {
+ return [
+ //
+ ];
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => ListBanners::route('/'),
+ 'create' => CreateBanner::route('/create'),
+ 'edit' => EditBanner::route('/{record}/edit'),
+ ];
+ }
+}
diff --git a/admin/app/Filament/Resources/BannerResource/Pages/CreateBanner.php b/admin/app/Filament/Resources/BannerResource/Pages/CreateBanner.php
new file mode 100644
index 00000000..e0bc1ce9
--- /dev/null
+++ b/admin/app/Filament/Resources/BannerResource/Pages/CreateBanner.php
@@ -0,0 +1,13 @@
+getResource()::getUrl('index');
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ DeleteAction::make(),
+ ];
+ }
+}
diff --git a/admin/app/Filament/Resources/BannerResource/Pages/ListBanners.php b/admin/app/Filament/Resources/BannerResource/Pages/ListBanners.php
new file mode 100644
index 00000000..97fd4c94
--- /dev/null
+++ b/admin/app/Filament/Resources/BannerResource/Pages/ListBanners.php
@@ -0,0 +1,21 @@
+components([
TextInput::make('name')
->required()
- ->maxLength(255),
+ ->maxLength(255)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Internal name for this coupon. Not shown to customers.'),
TextInput::make('code')
->required()
->maxLength(255)
- ->unique(Coupon::class, 'code', ignoreRecord: true),
- Toggle::make('is_percent'),
+ ->unique(Coupon::class, 'code', ignoreRecord: true)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The code the customer enters at checkout. Must be unique.'),
+ Toggle::make('is_percent')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When on, the amount is a percentage (e.g. 20 = 20% off). When off, it is a fixed amount in Tomans.'),
TextInput::make('amount')
->required()
- ->numeric(),
+ ->numeric()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Discount value. Interpreted as percent or fixed amount based on the toggle above.'),
TextInput::make('min_price')
->numeric()
- ->nullable(),
+ ->nullable()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Minimum cart total required to use this coupon. Leave empty for no minimum.'),
TextInput::make('max_discount')
->numeric()
- ->nullable(),
+ ->nullable()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Maximum discount amount even if the percentage would give more. Leave empty for no cap.'),
TextInput::make('total_used')
->required()
->numeric()
- ->default(0),
+ ->default(0)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Total times this coupon has been used. Usually managed automatically.'),
TextInput::make('total_uses')
->numeric()
- ->nullable(),
- Toggle::make('shipping'),
+ ->nullable()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Maximum total uses across all customers. Leave empty for unlimited.'),
+ Toggle::make('shipping')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When on, this coupon also grants free shipping.'),
Select::make('status')
->required()
->options(CouponStatusEnum::options())
@@ -67,39 +86,61 @@ public static function form(Schema $schema): Schema
Select::make('is_for')
->required()
->options(CouponForEnum::options())
- ->default(CouponForEnum::EVERYONE->value),
- DateTimePicker::make('started_at'),
- DateTimePicker::make('expired_at'),
+ ->default(CouponForEnum::EVERYONE->value)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Restricts who can use this coupon: everyone, registered users only, or partners only.'),
+ DateTimePicker::make('started_at')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When the coupon becomes valid. Leave empty to activate immediately.'),
+ DateTimePicker::make('expired_at')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When the coupon expires. Leave empty for no expiry.'),
Select::make('user_id')
->relationship('user', 'email')
->searchable()
->preload()
- ->native(false),
+ ->native(false)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Lock this coupon to one specific user. Leave empty for any user.'),
Select::make('user_creator_id')
->relationship('userCreator', 'email')
->searchable()
->preload()
- ->native(false),
+ ->native(false)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The admin user who created this coupon.'),
Select::make('seller_creator_id')
->relationship('sellerCreator', 'email')
->searchable()
->preload()
- ->native(false),
+ ->native(false)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The seller who created this coupon, if applicable.'),
Select::make('products')
->relationship('products', 'heading')
->multiple()
->searchable()
- ->preload(),
+ ->preload()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Scope this coupon to specific products only. Leave empty to apply to all products.'),
Select::make('varieties')
->relationship('varieties', 'attribute_value')
+ ->getOptionLabelFromRecordUsing(fn (Variety $record): string => implode(' — ', array_filter([
+ $record->product->heading,
+ $record->attribute_value ?? ('Variety #' . $record->id),
+ ])))
->multiple()
->searchable()
- ->preload(),
+ ->preload()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Scope this coupon to specific varieties only. Leave empty to apply to all varieties.'),
Select::make('categories')
->relationship('categories', 'heading')
->multiple()
->searchable()
- ->preload(),
+ ->preload()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Scope this coupon to specific categories only. Leave empty to apply to all categories.'),
]);
}
diff --git a/admin/app/Filament/Resources/CouponResource/Pages/ListCoupons.php b/admin/app/Filament/Resources/CouponResource/Pages/ListCoupons.php
index 472221a7..fdff5e5f 100644
--- a/admin/app/Filament/Resources/CouponResource/Pages/ListCoupons.php
+++ b/admin/app/Filament/Resources/CouponResource/Pages/ListCoupons.php
@@ -12,6 +12,8 @@ class ListCoupons extends ListRecords
{
protected static string $resource = CouponResource::class;
+ protected ?string $subheading = 'Coupons are manual discount codes entered by the customer at checkout. Unlike auto-applied discounts, a coupon requires a code. You can limit a coupon by minimum cart price, maximum discount cap, total usage, audience, and time window. Optionally scope it to specific products, varieties, or categories.';
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Filament/Resources/DiscountResource.php b/admin/app/Filament/Resources/DiscountResource.php
index 040eceef..4f613252 100644
--- a/admin/app/Filament/Resources/DiscountResource.php
+++ b/admin/app/Filament/Resources/DiscountResource.php
@@ -9,6 +9,7 @@
use App\Filament\Resources\DiscountResource\Pages\EditDiscount;
use App\Filament\Resources\DiscountResource\Pages\ListDiscounts;
use App\Models\Discount;
+use App\Models\Variety;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
@@ -26,7 +27,7 @@ class DiscountResource extends Resource
{
protected static ?string $model = Discount::class;
- protected static string | \UnitEnum | null $navigationGroup = 'Product';
+ protected static string | \UnitEnum | null $navigationGroup = 'Promotions';
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-receipt-percent';
@@ -35,7 +36,13 @@ public static function form(Schema $schema): Schema
return $schema
->components([
Select::make('variety_id')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The specific variety this discount applies to. Label shows: Product — Variety.')
->relationship('variety', 'attribute_value')
+ ->getOptionLabelFromRecordUsing(fn (Variety $record): string => implode(' — ', array_filter([
+ $record->product->heading,
+ $record->attribute_value ?? ('Variety #' . $record->id),
+ ])))
->searchable()
->preload()
->native(false)
@@ -43,31 +50,51 @@ public static function form(Schema $schema): Schema
TextInput::make('quantity')
->required()
->numeric()
- ->default(1),
+ ->default(1)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Minimum quantity the customer must purchase for this discount to apply.'),
TextInput::make('priority')
->required()
->numeric()
- ->default(0),
- Toggle::make('is_percent'),
+ ->default(0)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When multiple discounts qualify, the one with the highest priority wins.'),
+ Toggle::make('is_percent')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When on, the amount is treated as a percentage (e.g. 20 = 20% off). When off, it is a fixed amount in Tomans.'),
TextInput::make('amount')
->required()
- ->numeric(),
- DateTimePicker::make('started_at'),
- DateTimePicker::make('ended_at'),
+ ->numeric()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The discount value. Interpreted as percent or fixed amount based on the toggle above.'),
+ DateTimePicker::make('started_at')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When the discount becomes active. Leave empty to start immediately.'),
+ DateTimePicker::make('ended_at')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When the discount expires. Leave empty for no expiry.'),
TextInput::make('sold')
->required()
->numeric()
- ->default(0),
+ ->default(0)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Number of times this discount has already been used. Usually managed automatically.'),
TextInput::make('max_sell')
->numeric()
- ->nullable(),
+ ->nullable()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Total usage cap across all customers. Leave empty for unlimited.'),
TextInput::make('max_sell_by_user')
->numeric()
- ->nullable(),
+ ->nullable()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Maximum times a single user can use this discount. Leave empty for unlimited.'),
Select::make('is_for')
->required()
->options(DiscountForEnum::options())
- ->default(DiscountForEnum::EVERYONE->value),
+ ->default(DiscountForEnum::EVERYONE->value)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Restricts who can benefit from this discount: everyone, registered users only, or partners only.'),
]);
}
diff --git a/admin/app/Filament/Resources/DiscountResource/Pages/ListDiscounts.php b/admin/app/Filament/Resources/DiscountResource/Pages/ListDiscounts.php
index 5a180f5a..f43ce718 100644
--- a/admin/app/Filament/Resources/DiscountResource/Pages/ListDiscounts.php
+++ b/admin/app/Filament/Resources/DiscountResource/Pages/ListDiscounts.php
@@ -12,6 +12,8 @@ class ListDiscounts extends ListRecords
{
protected static string $resource = DiscountResource::class;
+ protected ?string $subheading = 'Discounts are automatic price rules applied to a specific variety when their conditions are met — no code needed. Each discount targets one variety and can be limited by quantity, time window, audience, and usage cap. When an order qualifies, the discount amount is stored on the order for historical reference.';
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Filament/Resources/ProductResource.php b/admin/app/Filament/Resources/ProductResource.php
index 3ea8fb0d..a1e85c89 100644
--- a/admin/app/Filament/Resources/ProductResource.php
+++ b/admin/app/Filament/Resources/ProductResource.php
@@ -11,6 +11,9 @@
use App\Filament\Resources\ProductResource\Pages\EditProduct;
use App\Filament\Resources\ProductResource\Pages\ListProducts;
use App\Models\Attribute;
+use App\Models\Attribute as AttributeModel;
+use App\Models\AttributeGroup;
+use App\Models\AttributeGroupCategory;
use App\Models\Product;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
@@ -23,13 +26,14 @@
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
-use Filament\Schemas\Components\Fieldset;
+use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
+use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
@@ -37,7 +41,7 @@ class ProductResource extends Resource
{
protected static ?string $model = Product::class;
- protected static string | \UnitEnum | null $navigationGroup = 'Product';
+ protected static string | \UnitEnum | null $navigationGroup = 'Catalog';
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-shopping-bag';
@@ -59,22 +63,34 @@ public static function form(Schema $schema): Schema
->dehydrated()
->required()
->maxLength(255)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Auto-generated from the heading on create. Cannot be changed after creation.')
->unique(Product::class, 'slug', ignoreRecord: true),
TextInput::make('price')
->required()
->numeric()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Base price shown when no variety is selected.')
->prefix('تومان'),
TinyEditor::make('content')
->columnSpanFull()
->required(),
TextInput::make('title')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('SEO
tag. If empty, the heading is used.')
->maxLength(255),
Textarea::make('description')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('SEO meta description shown in search results.')
->maxLength(255)
->columnSpanFull(),
Toggle::make('no_index')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When on, adds a noindex meta tag so search engines skip this page.')
->required(),
TextInput::make('canonical')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Canonical URL to prevent duplicate content. Leave empty unless this product mirrors another page.')
->maxLength(255),
Repeater::make('images')
->relationship('images')
@@ -95,13 +111,19 @@ public static function form(Schema $schema): Schema
Select::make('attribute_group_id')
->relationship('attributeGroup', 'name')
+ ->searchable()
->native(false)
- ->preload(),
+ ->live()
+ ->preload()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Defines which attribute group differentiates the varieties of this product (e.g. "Color"). Changing this reloads the attribute options in the variety rows below.'),
Select::make('category_id')
->relationship('category', 'heading')
->required()
->native(false)
- ->preload(),
+ ->preload()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The category this product belongs to. Required attribute groups for this category will be enforced on save.'),
Select::make('brand_id')
->relationship('brand', 'heading')
->required()
@@ -110,20 +132,30 @@ public static function form(Schema $schema): Schema
TextInput::make('minimum')
->required()
->numeric()
- ->default(1),
+ ->default(1)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Minimum quantity a customer can add to their cart.'),
TextInput::make('maximum')
- ->numeric(),
+ ->numeric()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Maximum quantity per order. Leave empty for no limit.'),
TextInput::make('step')
->required()
->numeric()
- ->default(1),
+ ->default(1)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Quantity increment step (e.g. 2 means customers can add 2, 4, 6...).'),
TextInput::make('profit_percent')
->required()
->numeric()
->suffix('%')
- ->default(0),
+ ->default(0)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('ShopFlow\'s commission percentage from each sale of this product.'),
Repeater::make('attributes')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Product-level attributes that describe this product (e.g. material, dimensions). These are not varieties — they are shared across all varieties. Mark an attribute as Highlight to feature it prominently on the product page.')
->schema([
Select::make('attribute_id')
->label('Attribute')
@@ -138,7 +170,6 @@ public static function form(Schema $schema): Schema
Checkbox::make('pivot.is_highlight')
->label('Highlight'),
])
- ->dehydrated(false)
->afterStateHydrated(function (mixed $state, callable $set, Component $livewire): void {
if (isset($livewire->record) && $livewire->record) {
$attributes = $livewire->record->attributes()->get();
@@ -192,35 +223,48 @@ public static function form(Schema $schema): Schema
->required()
->numeric()
->default(0),
- Fieldset::make('Variety Details')
+ Repeater::make('varieties')
+ ->label('Variety Details')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Each row is a variety of this product. The attribute options depend on the Attribute Group selected above — set that first.')
+ ->relationship('varieties')
+ ->columnSpanFull()
->schema([
- Repeater::make('varieties')
- ->relationship('varieties')
- ->schema([
- TextInput::make('attribute_value')
- ->maxLength(255),
- TextInput::make('color')
- ->maxLength(255),
- TextInput::make('price')
- ->required()
- ->numeric(),
- TextInput::make('sale_price')
- ->numeric()
- ->nullable(),
- TextInput::make('inventory')
- ->required()
- ->numeric()
- ->default(0),
- Toggle::make('has_stock')
- ->default(true)
- ->required(),
- Select::make('status')
- ->required()
- ->options(VarietyStatusEnum::options())
- ->default(VarietyStatusEnum::PUBLISHED->value),
- ])
- ->columnSpanFull(),
- ]),
+ Select::make('attribute_id')
+ ->label('Attribute')
+ ->options(function (Get $get): array {
+ $groupId = $get('../../attribute_group_id');
+ if (! $groupId) {
+ return [];
+ }
+
+ return AttributeModel::query()
+ ->where('attribute_group_id', $groupId)
+ ->pluck('value', 'id')
+ ->toArray();
+ })
+ ->searchable()
+ ->nullable()
+ ->helperText('Selecting an attribute auto-fills the value and color.'),
+ TextInput::make('price')
+ ->required()
+ ->numeric(),
+ TextInput::make('sale_price')
+ ->numeric()
+ ->nullable(),
+ TextInput::make('inventory')
+ ->required()
+ ->numeric()
+ ->default(0),
+ Toggle::make('has_stock')
+ ->default(true)
+ ->required(),
+ Select::make('status')
+ ->required()
+ ->options(VarietyStatusEnum::options())
+ ->default(VarietyStatusEnum::PUBLISHED->value),
+ ])
+ ->columnSpanFull(),
]);
}
@@ -310,6 +354,38 @@ public static function table(Table $table): Table
]);
}
+ /**
+ * Returns names of required attribute groups that have no selected attribute.
+ *
+ * @param array $attributeState
+ * @return Collection
+ */
+ public static function missingRequiredGroups(array $attributeState, int $categoryId): Collection
+ {
+ $selectedAttributeIds = collect($attributeState)
+ ->filter(fn (array $item): bool => ! empty($item['attribute_id']))
+ ->pluck('attribute_id')
+ ->map(fn (mixed $id): int => (int) $id)
+ ->all();
+
+ $requiredGroupIds = AttributeGroupCategory::query()
+ ->where('category_id', $categoryId)
+ ->where('required', true)
+ ->pluck('attribute_group_id');
+
+ if ($requiredGroupIds->isEmpty()) {
+ return collect();
+ }
+
+ $selectedGroupIds = Attribute::query()
+ ->whereIn('id', $selectedAttributeIds)
+ ->pluck('attribute_group_id');
+
+ return AttributeGroup::query()
+ ->whereIn('id', $requiredGroupIds->diff($selectedGroupIds))
+ ->pluck('name');
+ }
+
public static function getRelations(): array
{
return [
diff --git a/admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php b/admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php
index 2aae6a31..485b184d 100644
--- a/admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php
+++ b/admin/app/Filament/Resources/ProductResource/Pages/CreateProduct.php
@@ -5,9 +5,41 @@
namespace App\Filament\Resources\ProductResource\Pages;
use App\Filament\Resources\ProductResource;
+use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
class CreateProduct extends CreateRecord
{
protected static string $resource = ProductResource::class;
+
+ protected function beforeCreate(): void
+ {
+ $categoryId = (int) ($this->data['category_id'] ?? 0);
+
+ if ($categoryId === 0) {
+ return;
+ }
+
+ $missing = ProductResource::missingRequiredGroups(
+ $this->data['attributes'] ?? [],
+ $categoryId,
+ );
+
+ if ($missing->isNotEmpty()) {
+ Notification::make()
+ ->danger()
+ ->title('Required attribute groups missing')
+ ->body('Add at least one attribute from: ' . $missing->join(', '))
+ ->send();
+
+ $this->halt();
+ }
+ }
+
+ protected function mutateFormDataBeforeCreate(array $data): array
+ {
+ unset($data['attributes']);
+
+ return $data;
+ }
}
diff --git a/admin/app/Filament/Resources/ProductResource/Pages/EditProduct.php b/admin/app/Filament/Resources/ProductResource/Pages/EditProduct.php
index 36d920a3..3476114a 100644
--- a/admin/app/Filament/Resources/ProductResource/Pages/EditProduct.php
+++ b/admin/app/Filament/Resources/ProductResource/Pages/EditProduct.php
@@ -6,12 +6,44 @@
use App\Filament\Resources\ProductResource;
use Filament\Actions\DeleteAction;
+use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
class EditProduct extends EditRecord
{
protected static string $resource = ProductResource::class;
+ protected function beforeSave(): void
+ {
+ $categoryId = (int) ($this->data['category_id'] ?? 0);
+
+ if ($categoryId === 0) {
+ return;
+ }
+
+ $missing = ProductResource::missingRequiredGroups(
+ $this->data['attributes'] ?? [],
+ $categoryId,
+ );
+
+ if ($missing->isNotEmpty()) {
+ Notification::make()
+ ->danger()
+ ->title('Required attribute groups missing')
+ ->body('Add at least one attribute from: ' . $missing->join(', '))
+ ->send();
+
+ $this->halt();
+ }
+ }
+
+ protected function mutateFormDataBeforeSave(array $data): array
+ {
+ unset($data['attributes']);
+
+ return $data;
+ }
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Filament/Resources/ProductResource/Pages/ListProducts.php b/admin/app/Filament/Resources/ProductResource/Pages/ListProducts.php
index 5e10233d..04523179 100644
--- a/admin/app/Filament/Resources/ProductResource/Pages/ListProducts.php
+++ b/admin/app/Filament/Resources/ProductResource/Pages/ListProducts.php
@@ -12,6 +12,8 @@ class ListProducts extends ListRecords
{
protected static string $resource = ProductResource::class;
+ protected ?string $subheading = 'Products are the items sold on ShopFlow. Each product belongs to a category and brand, has a base price, and can have multiple varieties (e.g. different colors or sizes) each with their own price and inventory. Set an attribute group to define which attribute type differentiates the varieties. Product-level attributes describe shared specifications shown on the product page.';
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Filament/Resources/VarietyResource.php b/admin/app/Filament/Resources/VarietyResource.php
index c35bcc25..efa48a8d 100644
--- a/admin/app/Filament/Resources/VarietyResource.php
+++ b/admin/app/Filament/Resources/VarietyResource.php
@@ -8,6 +8,8 @@
use App\Filament\Resources\VarietyResource\Pages\CreateVariety;
use App\Filament\Resources\VarietyResource\Pages\EditVariety;
use App\Filament\Resources\VarietyResource\Pages\ListVarieties;
+use App\Models\Attribute;
+use App\Models\Product;
use App\Models\Variety;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
@@ -16,6 +18,7 @@
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
+use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
@@ -25,7 +28,7 @@ class VarietyResource extends Resource
{
protected static ?string $model = Variety::class;
- protected static string | \UnitEnum | null $navigationGroup = 'Product';
+ protected static string | \UnitEnum | null $navigationGroup = 'Catalog';
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-shopping-bag';
@@ -36,23 +39,52 @@ public static function form(Schema $schema): Schema
Select::make('product_id')
->relationship('product', 'heading')
->searchable()
- ->required(),
- TextInput::make('attribute_value')
- ->maxLength(255),
- TextInput::make('color')
- ->maxLength(255),
+ ->live()
+ ->required()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The product this variety belongs to. Changing it reloads the available attributes below.'),
+ Select::make('attribute_id')
+ ->label('Attribute')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The attribute that defines this variety (e.g. "Red" from the "Color" group). Auto-fills value and color on save. Options are filtered by the product\'s attribute group.')
+ ->options(function (Get $get, ?Variety $record): array {
+ $productId = $get('product_id') ?? $record?->product_id;
+ if (! $productId) {
+ return [];
+ }
+ $product = Product::find($productId);
+ if (! $product?->attribute_group_id) {
+ return [];
+ }
+
+ return Attribute::query()
+ ->where('attribute_group_id', $product->attribute_group_id)
+ ->pluck('value', 'id')
+ ->toArray();
+ })
+ ->searchable()
+ ->nullable()
+ ->helperText('Selecting an attribute auto-fills the value and color.'),
TextInput::make('price')
->required()
->numeric()
- ->prefix('$'),
+ ->prefix('تومان')
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('The selling price of this variety.'),
TextInput::make('sale_price')
- ->numeric(),
+ ->numeric()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Discounted price shown instead of the regular price when set. Leave empty for no sale price.'),
TextInput::make('inventory')
->required()
->numeric()
- ->default(0),
+ ->default(0)
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('Number of units available in stock.'),
Toggle::make('has_stock')
- ->required(),
+ ->required()
+ ->hintIcon('heroicon-o-information-circle')
+ ->hintIconTooltip('When off, this variety is shown as out of stock regardless of inventory count.'),
Select::make('status')
->required()
->options(VarietyStatusEnum::options())
@@ -68,7 +100,11 @@ public static function table(Table $table): Table
->limit(30)
->wrap()
->searchable(),
- TextColumn::make('attribute_value'),
+ TextColumn::make('attribute.value')
+ ->label('Attribute')
+ ->searchable(),
+ TextColumn::make('attribute_value')
+ ->label('Value'),
TextColumn::make('color'),
TextColumn::make('price')
->money(),
diff --git a/admin/app/Filament/Resources/VarietyResource/Pages/ListVarieties.php b/admin/app/Filament/Resources/VarietyResource/Pages/ListVarieties.php
index 926f3963..5a5dfb76 100644
--- a/admin/app/Filament/Resources/VarietyResource/Pages/ListVarieties.php
+++ b/admin/app/Filament/Resources/VarietyResource/Pages/ListVarieties.php
@@ -12,6 +12,8 @@ class ListVarieties extends ListRecords
{
protected static string $resource = VarietyResource::class;
+ protected ?string $subheading = 'Varieties are the purchasable options of a product (e.g. a T-shirt in Red or Blue, each with its own price and inventory). Each variety belongs to one product and is linked to one attribute from the product\'s attribute group. Selecting an attribute auto-fills the variety\'s label and color.';
+
protected function getHeaderActions(): array
{
return [
diff --git a/admin/app/Models/Banner.php b/admin/app/Models/Banner.php
new file mode 100644
index 00000000..b46618d1
--- /dev/null
+++ b/admin/app/Models/Banner.php
@@ -0,0 +1,60 @@
+ $images
+ */
+class Banner extends Model
+{
+ /** @use HasFactory */
+ use HasFactory;
+
+ protected $fillable = [
+ 'position',
+ 'heading',
+ 'url',
+ 'sort',
+ 'status',
+ ];
+
+ protected $casts = [
+ 'status' => BannerStatusEnum::class,
+ ];
+
+ protected static function booted(): void
+ {
+ static::deleting(function (Banner $banner): void {
+ $banner->images()->delete();
+ });
+ }
+
+ public function images(): MorphMany
+ {
+ return $this->morphMany(Image::class, 'imageable');
+ }
+
+ public function featuredImage(): HasOne
+ {
+ return $this->hasOne(Image::class, 'imageable_id')
+ ->where('imageable_type', self::class)
+ ->where('is_featured', true);
+ }
+}
diff --git a/admin/app/Models/Variety.php b/admin/app/Models/Variety.php
index 66bdb4e0..2b001b6a 100644
--- a/admin/app/Models/Variety.php
+++ b/admin/app/Models/Variety.php
@@ -11,14 +11,17 @@
/**
* @property positive-int $id
- * @property Product $product
- * @property string $attribute_value
+ * @property positive-int $product_id
+ * @property positive-int|null $attribute_id
+ * @property string|null $attribute_value
* @property string|null $color
* @property positive-int $price
* @property positive-int|null $sale_price
* @property positive-int $inventory
* @property bool $has_stock
* @property VarietyStatusEnum $status
+ * @property Product $product
+ * @property Attribute|null $attribute
*/
class Variety extends Model
{
@@ -26,6 +29,7 @@ class Variety extends Model
protected $fillable = [
'product_id',
+ 'attribute_id',
'attribute_value',
'color',
'price',
@@ -42,6 +46,18 @@ class Variety extends Model
protected static function booted(): void
{
+ static::saving(function (Variety $variety): void {
+ if ($variety->attribute_id === null) {
+ return;
+ }
+ $attribute = Attribute::find($variety->attribute_id);
+ if ($attribute === null) {
+ return;
+ }
+ $variety->attribute_value = $attribute->value;
+ $variety->color = $attribute->color;
+ });
+
static::saved(fn (Variety $variety) => $variety->syncProductVarietyCount());
static::deleted(fn (Variety $variety) => $variety->syncProductVarietyCount());
}
@@ -60,10 +76,13 @@ public function syncProductVarietyCount(): void
])->saveQuietly();
}
- // Relationships
-
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
+
+ public function attribute(): BelongsTo
+ {
+ return $this->belongsTo(Attribute::class);
+ }
}
diff --git a/admin/config/livewire.php b/admin/config/livewire.php
index 9b2cf87f..93f067ef 100644
--- a/admin/config/livewire.php
+++ b/admin/config/livewire.php
@@ -6,41 +6,47 @@
/*
|---------------------------------------------------------------------------
- | Class Namespace
+ | Component Locations
|---------------------------------------------------------------------------
|
- | This value sets the root class namespace for Livewire component classes in
- | your application. This value will change where component auto-discovery
- | finds components. It's also referenced by the file creation commands.
+ | This value sets the root directories that'll be used to resolve view-based
+ | components like single and multi-file components. The make command will
+ | use the first directory in this array to add new component files to.
|
*/
- 'class_namespace' => 'App\\Livewire',
+ 'component_locations' => [
+ resource_path('views/components'),
+ resource_path('views/livewire'),
+ ],
/*
|---------------------------------------------------------------------------
- | View Path
+ | Component Namespaces
|---------------------------------------------------------------------------
|
- | This value is used to specify where Livewire component Blade templates are
- | stored when running file creation commands like `artisan make:livewire`.
- | It is also used if you choose to omit a component's render() method.
+ | This value sets default namespaces that will be used to resolve view-based
+ | components like single-file and multi-file components. These folders'll
+ | also be referenced when creating new components via the make command.
|
*/
- 'view_path' => resource_path('views/livewire'),
+ 'component_namespaces' => [
+ 'layouts' => resource_path('views/layouts'),
+ 'pages' => resource_path('views/pages'),
+ ],
/*
|---------------------------------------------------------------------------
- | Layout
+ | Page Layout
|---------------------------------------------------------------------------
- | The view that will be used as the layout when rendering a single component
- | as an entire page via `Route::get('/post/create', CreatePost::class);`.
- | In this case, the view returned by CreatePost will render into $slot.
+ | The view that will be used as the layout when rendering a single component as
+ | an entire page via `Route::livewire('/post/create', 'pages::create-post')`.
+ | In this case, the content of pages::create-post will render into $slot.
|
*/
- 'layout' => 'components.layouts.app',
+ 'component_layout' => 'layouts::app',
/*
|---------------------------------------------------------------------------
@@ -52,7 +58,66 @@
|
*/
- 'lazy_placeholder' => null,
+ 'component_placeholder' => null, // Example: 'placeholders::skeleton'
+
+ /*
+ |---------------------------------------------------------------------------
+ | Make Command
+ |---------------------------------------------------------------------------
+ | This value determines the default configuration for the artisan make command
+ | You can configure the component type (sfc, mfc, class) and whether to use
+ | the high-voltage (⚡) emoji as a prefix in the sfc|mfc component names.
+ |
+ */
+
+ 'make_command' => [
+ 'type' => 'sfc', // Options: 'sfc', 'mfc', 'class'
+ 'emoji' => true, // Options: true, false
+ 'with' => [
+ 'js' => false,
+ 'css' => false,
+ 'test' => false,
+ ],
+ ],
+
+ /*
+ |---------------------------------------------------------------------------
+ | Class Namespace
+ |---------------------------------------------------------------------------
+ |
+ | This value sets the root class namespace for Livewire component classes in
+ | your application. This value will change where component auto-discovery
+ | finds components. It's also referenced by the file creation commands.
+ |
+ */
+
+ 'class_namespace' => 'App\\Livewire',
+
+ /*
+ |---------------------------------------------------------------------------
+ | Class Path
+ |---------------------------------------------------------------------------
+ |
+ | This value is used to specify the path where Livewire component class files
+ | are created when running creation commands like `artisan make:livewire`.
+ | This path is customizable to match your projects directory structure.
+ |
+ */
+
+ 'class_path' => app_path('Livewire'),
+
+ /*
+ |---------------------------------------------------------------------------
+ | View Path
+ |---------------------------------------------------------------------------
+ |
+ | This value is used to specify where Livewire component Blade templates are
+ | stored when running file creation commands like `artisan make:livewire`.
+ | It is also used if you choose to omit a component's render() method.
+ |
+ */
+
+ 'view_path' => resource_path('views/livewire'),
/*
|---------------------------------------------------------------------------
@@ -66,11 +131,11 @@
*/
'temporary_file_upload' => [
- 'disk' => null, // Example: 'local', 's3' | Default: 'default'
- 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
- 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
- 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
- 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
+ 'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default'
+ 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
+ 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
+ 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
+ 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
@@ -147,6 +212,19 @@
'inject_morph_markers' => true,
+ /*
+ |---------------------------------------------------------------------------
+ | Smart Wire Keys
+ |---------------------------------------------------------------------------
+ |
+ | Livewire uses loops and keys used within loops to generate smart keys that
+ | are applied to nested components that don't have them. This makes using
+ | nested components more reliable by ensuring that they all have keys.
+ |
+ */
+
+ 'smart_wire_keys' => true,
+
/*
|---------------------------------------------------------------------------
| Pagination Theme
@@ -159,4 +237,48 @@
*/
'pagination_theme' => 'tailwind',
+
+ /*
+ |---------------------------------------------------------------------------
+ | Release Token
+ |---------------------------------------------------------------------------
+ |
+ | This token is stored client-side and sent along with each request to check
+ | a users session to see if a new release has invalidated it. If there is
+ | a mismatch it will throw an error and prompt for a browser refresh.
+ |
+ */
+
+ 'release_token' => 'a',
+
+ /*
+ |---------------------------------------------------------------------------
+ | CSP Safe
+ |---------------------------------------------------------------------------
+ |
+ | This config is used to determine if Livewire will use the CSP-safe version
+ | of Alpine in its bundle. This is useful for applications that are using
+ | strict Content Security Policy (CSP) to protect against XSS attacks.
+ |
+ */
+
+ 'csp_safe' => false,
+
+ /*
+ |---------------------------------------------------------------------------
+ | Payload Guards
+ |---------------------------------------------------------------------------
+ |
+ | These settings protect against malicious or oversized payloads that could
+ | cause denial of service. The default values should feel reasonable for
+ | most web applications. Each can be set to null to disable the limit.
+ |
+ */
+
+ 'payload' => [
+ 'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes
+ 'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths
+ 'max_calls' => 50, // Maximum method calls per request
+ 'max_components' => 200, // Maximum components per batch request
+ ],
];
diff --git a/admin/database/factories/BannerFactory.php b/admin/database/factories/BannerFactory.php
new file mode 100644
index 00000000..64c0f32f
--- /dev/null
+++ b/admin/database/factories/BannerFactory.php
@@ -0,0 +1,45 @@
+
+ */
+class BannerFactory extends Factory
+{
+ protected $model = Banner::class;
+
+ /**
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ 'position' => fake()->randomElement(['home-top', 'home-middle', 'category-side']),
+ 'heading' => fake()->words(3, true),
+ 'url' => fake()->url(),
+ 'sort' => fake()->numberBetween(0, 20),
+ 'status' => fake()->randomElement(BannerStatusEnum::cases()),
+ ];
+ }
+
+ public function withImages(int $count = 3): static
+ {
+ return $this->afterCreating(function (Banner $banner) use ($count) {
+ for ($i = 0; $i < $count; $i++) {
+ $banner->images()->create([
+ 'path' => fake()->imageUrl(),
+ 'is_featured' => $i === 0,
+ 'order' => $i,
+ 'alt_text' => fake()->words(2, true),
+ ]);
+ }
+ });
+ }
+}
diff --git a/admin/database/factories/VarietyFactory.php b/admin/database/factories/VarietyFactory.php
index 65761ce1..06f549dc 100644
--- a/admin/database/factories/VarietyFactory.php
+++ b/admin/database/factories/VarietyFactory.php
@@ -5,25 +5,40 @@
namespace Database\Factories;
use App\Enums\VarietyStatusEnum;
+use App\Models\Attribute;
use App\Models\Product;
use App\Models\Variety;
use Illuminate\Database\Eloquent\Factories\Factory;
+/**
+ * @extends Factory
+ */
class VarietyFactory extends Factory
{
protected $model = Variety::class;
+ /**
+ * @return array
+ */
public function definition(): array
{
return [
'product_id' => Product::factory(),
- 'attribute_value' => $this->faker->word(),
- 'color' => $this->faker->safeColorName(),
- 'price' => $this->faker->numberBetween(1000, 100000),
- 'sale_price' => $this->faker->optional()->numberBetween(500, 90000),
- 'inventory' => $this->faker->numberBetween(0, 100),
- 'has_stock' => $this->faker->boolean(80), // 80% chance of being true
+ 'attribute_id' => null,
+ 'attribute_value' => fake()->word(),
+ 'color' => fake()->safeColorName(),
+ 'price' => fake()->numberBetween(1000, 100000),
+ 'sale_price' => fake()->optional()->numberBetween(500, 90000),
+ 'inventory' => fake()->numberBetween(0, 100),
+ 'has_stock' => fake()->boolean(80),
'status' => fake()->randomElement(VarietyStatusEnum::cases()),
];
}
+
+ public function withAttribute(Attribute $attribute): static
+ {
+ return $this->state([
+ 'attribute_id' => $attribute->id,
+ ]);
+ }
}
diff --git a/admin/database/migrations/2024_12_23_185231_create_attribute_group_category_table.php b/admin/database/migrations/2024_12_23_185231_create_attribute_group_category_table.php
index df1cb06b..ca4e88c3 100644
--- a/admin/database/migrations/2024_12_23_185231_create_attribute_group_category_table.php
+++ b/admin/database/migrations/2024_12_23_185231_create_attribute_group_category_table.php
@@ -20,7 +20,6 @@ public function up(): void
$table->foreignIdFor(AttributeGroup::class);
$table->foreignIdFor(Category::class);
$table->boolean('as_filter')->default(false);
- // todo : implement in product resource
$table->boolean('required')->default(false);
$table->timestamps();
});
diff --git a/admin/database/migrations/2025_08_15_132349_create_varieties_table.php b/admin/database/migrations/2025_08_15_132349_create_varieties_table.php
index 30202277..185188a9 100644
--- a/admin/database/migrations/2025_08_15_132349_create_varieties_table.php
+++ b/admin/database/migrations/2025_08_15_132349_create_varieties_table.php
@@ -3,6 +3,7 @@
declare(strict_types=1);
use App\Enums\VarietyStatusEnum;
+use App\Models\Attribute;
use App\Models\Product;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@@ -18,6 +19,7 @@ public function up(): void
Schema::create('varieties', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Product::class)->constrained()->cascadeOnDelete();
+ $table->foreignIdFor(Attribute::class)->nullable()->constrained()->nullOnDelete();
$table->string('attribute_value')->nullable();
$table->string('color')->nullable();
$table->decimal('price', 10, 2);
diff --git a/admin/database/migrations/2026_06_19_000006_create_banners_table.php b/admin/database/migrations/2026_06_19_000006_create_banners_table.php
new file mode 100644
index 00000000..55fb752a
--- /dev/null
+++ b/admin/database/migrations/2026_06_19_000006_create_banners_table.php
@@ -0,0 +1,35 @@
+id();
+ $table->string('position');
+ $table->string('heading');
+ $table->string('url')->nullable();
+ $table->unsignedInteger('sort')->nullable();
+ $table->unsignedTinyInteger('status')->default(BannerStatusEnum::PUBLISHED->value);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('banners');
+ }
+};
diff --git a/admin/database/seeders/BannerSeeder.php b/admin/database/seeders/BannerSeeder.php
new file mode 100644
index 00000000..3e481db6
--- /dev/null
+++ b/admin/database/seeders/BannerSeeder.php
@@ -0,0 +1,17 @@
+truncate();
+ Banner::factory()->count(20)->withImages()->create();
+ }
+}
diff --git a/admin/database/seeders/TestSeeder.php b/admin/database/seeders/TestSeeder.php
index 6e657322..ec7ef176 100644
--- a/admin/database/seeders/TestSeeder.php
+++ b/admin/database/seeders/TestSeeder.php
@@ -17,6 +17,7 @@ public function run(): void
VarietySeeder::class,
DiscountSeeder::class,
CouponSeeder::class,
+ BannerSeeder::class,
]);
}
}
diff --git a/admin/public/vendor/livewire/livewire.csp.esm.js b/admin/public/vendor/livewire/livewire.csp.esm.js
new file mode 100644
index 00000000..2f713776
--- /dev/null
+++ b/admin/public/vendor/livewire/livewire.csp.esm.js
@@ -0,0 +1,16284 @@
+var __create = Object.create;
+var __defProp = Object.defineProperty;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getProtoOf = Object.getPrototypeOf;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __commonJS = (cb, mod) => function __require() {
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+var __copyProps = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames(from))
+ if (!__hasOwnProp.call(to, key) && key !== except)
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
+ }
+ return to;
+};
+var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
+ mod
+));
+
+// node_modules/@alpinejs/csp/dist/module.cjs.js
+var require_module_cjs = __commonJS({
+ "node_modules/@alpinejs/csp/dist/module.cjs.js"(exports, module) {
+ var __create2 = Object.create;
+ var __defProp2 = Object.defineProperty;
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
+ var __getProtoOf2 = Object.getPrototypeOf;
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
+ var __commonJS2 = (cb, mod) => function __require() {
+ return mod || (0, cb[__getOwnPropNames2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+ };
+ var __export = (target, all2) => {
+ for (var name in all2)
+ __defProp2(target, name, { get: all2[name], enumerable: true });
+ };
+ var __copyProps2 = (to, from, except, desc) => {
+ if (from && typeof from === "object" || typeof from === "function") {
+ for (let key of __getOwnPropNames2(from))
+ if (!__hasOwnProp2.call(to, key) && key !== except)
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
+ }
+ return to;
+ };
+ var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__getProtoOf2(mod)) : {}, __copyProps2(
+ isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target,
+ mod
+ ));
+ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
+ var require_shared_cjs = __commonJS2({
+ "node_modules/@vue/shared/dist/shared.cjs.js"(exports2) {
+ "use strict";
+ Object.defineProperty(exports2, "__esModule", { value: true });
+ function makeMap(str, expectsLowerCase) {
+ const map = /* @__PURE__ */ Object.create(null);
+ const list = str.split(",");
+ for (let i = 0; i < list.length; i++) {
+ map[list[i]] = true;
+ }
+ return expectsLowerCase ? (val) => !!map[val.toLowerCase()] : (val) => !!map[val];
+ }
+ var PatchFlagNames = {
+ [1]: `TEXT`,
+ [2]: `CLASS`,
+ [4]: `STYLE`,
+ [8]: `PROPS`,
+ [16]: `FULL_PROPS`,
+ [32]: `HYDRATE_EVENTS`,
+ [64]: `STABLE_FRAGMENT`,
+ [128]: `KEYED_FRAGMENT`,
+ [256]: `UNKEYED_FRAGMENT`,
+ [512]: `NEED_PATCH`,
+ [1024]: `DYNAMIC_SLOTS`,
+ [2048]: `DEV_ROOT_FRAGMENT`,
+ [-1]: `HOISTED`,
+ [-2]: `BAIL`
+ };
+ var slotFlagsText = {
+ [1]: "STABLE",
+ [2]: "DYNAMIC",
+ [3]: "FORWARDED"
+ };
+ var GLOBALS_WHITE_LISTED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt";
+ var isGloballyWhitelisted = /* @__PURE__ */ makeMap(GLOBALS_WHITE_LISTED);
+ var range = 2;
+ function generateCodeFrame(source, start22 = 0, end = source.length) {
+ let lines = source.split(/(\r?\n)/);
+ const newlineSequences = lines.filter((_, idx) => idx % 2 === 1);
+ lines = lines.filter((_, idx) => idx % 2 === 0);
+ let count = 0;
+ const res = [];
+ for (let i = 0; i < lines.length; i++) {
+ count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0);
+ if (count >= start22) {
+ for (let j = i - range; j <= i + range || end > count; j++) {
+ if (j < 0 || j >= lines.length)
+ continue;
+ const line = j + 1;
+ res.push(`${line}${" ".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}`);
+ const lineLength = lines[j].length;
+ const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0;
+ if (j === i) {
+ const pad = start22 - (count - (lineLength + newLineSeqLength));
+ const length = Math.max(1, end > count ? lineLength - pad : end - start22);
+ res.push(` | ` + " ".repeat(pad) + "^".repeat(length));
+ } else if (j > i) {
+ if (end > count) {
+ const length = Math.max(Math.min(end - count, lineLength), 1);
+ res.push(` | ` + "^".repeat(length));
+ }
+ count += lineLength + newLineSeqLength;
+ }
+ }
+ break;
+ }
+ }
+ return res.join("\n");
+ }
+ var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;
+ var isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs);
+ var isBooleanAttr2 = /* @__PURE__ */ makeMap(specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected`);
+ var unsafeAttrCharRE = /[>/="'\u0009\u000a\u000c\u0020]/;
+ var attrValidationCache = {};
+ function isSSRSafeAttrName(name) {
+ if (attrValidationCache.hasOwnProperty(name)) {
+ return attrValidationCache[name];
+ }
+ const isUnsafe = unsafeAttrCharRE.test(name);
+ if (isUnsafe) {
+ console.error(`unsafe attribute name: ${name}`);
+ }
+ return attrValidationCache[name] = !isUnsafe;
+ }
+ var propsToAttrMap = {
+ acceptCharset: "accept-charset",
+ className: "class",
+ htmlFor: "for",
+ httpEquiv: "http-equiv"
+ };
+ var isNoUnitNumericStyleProp = /* @__PURE__ */ makeMap(`animation-iteration-count,border-image-outset,border-image-slice,border-image-width,box-flex,box-flex-group,box-ordinal-group,column-count,columns,flex,flex-grow,flex-positive,flex-shrink,flex-negative,flex-order,grid-row,grid-row-end,grid-row-span,grid-row-start,grid-column,grid-column-end,grid-column-span,grid-column-start,font-weight,line-clamp,line-height,opacity,order,orphans,tab-size,widows,z-index,zoom,fill-opacity,flood-opacity,stop-opacity,stroke-dasharray,stroke-dashoffset,stroke-miterlimit,stroke-opacity,stroke-width`);
+ var isKnownAttr = /* @__PURE__ */ makeMap(`accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap`);
+ function normalizeStyle(value) {
+ if (isArray2(value)) {
+ const res = {};
+ for (let i = 0; i < value.length; i++) {
+ const item = value[i];
+ const normalized = normalizeStyle(isString(item) ? parseStringStyle(item) : item);
+ if (normalized) {
+ for (const key in normalized) {
+ res[key] = normalized[key];
+ }
+ }
+ }
+ return res;
+ } else if (isObject22(value)) {
+ return value;
+ }
+ }
+ var listDelimiterRE = /;(?![^(]*\))/g;
+ var propertyDelimiterRE = /:(.+)/;
+ function parseStringStyle(cssText) {
+ const ret = {};
+ cssText.split(listDelimiterRE).forEach((item) => {
+ if (item) {
+ const tmp = item.split(propertyDelimiterRE);
+ tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());
+ }
+ });
+ return ret;
+ }
+ function stringifyStyle(styles) {
+ let ret = "";
+ if (!styles) {
+ return ret;
+ }
+ for (const key in styles) {
+ const value = styles[key];
+ const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key);
+ if (isString(value) || typeof value === "number" && isNoUnitNumericStyleProp(normalizedKey)) {
+ ret += `${normalizedKey}:${value};`;
+ }
+ }
+ return ret;
+ }
+ function normalizeClass(value) {
+ let res = "";
+ if (isString(value)) {
+ res = value;
+ } else if (isArray2(value)) {
+ for (let i = 0; i < value.length; i++) {
+ const normalized = normalizeClass(value[i]);
+ if (normalized) {
+ res += normalized + " ";
+ }
+ }
+ } else if (isObject22(value)) {
+ for (const name in value) {
+ if (value[name]) {
+ res += name + " ";
+ }
+ }
+ }
+ return res.trim();
+ }
+ var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot";
+ var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistanceLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view";
+ var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr";
+ var isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS);
+ var isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS);
+ var isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS);
+ var escapeRE = /["'&<>]/;
+ function escapeHtml(string) {
+ const str = "" + string;
+ const match = escapeRE.exec(str);
+ if (!match) {
+ return str;
+ }
+ let html = "";
+ let escaped;
+ let index;
+ let lastIndex = 0;
+ for (index = match.index; index < str.length; index++) {
+ switch (str.charCodeAt(index)) {
+ case 34:
+ escaped = """;
+ break;
+ case 38:
+ escaped = "&";
+ break;
+ case 39:
+ escaped = "'";
+ break;
+ case 60:
+ escaped = "<";
+ break;
+ case 62:
+ escaped = ">";
+ break;
+ default:
+ continue;
+ }
+ if (lastIndex !== index) {
+ html += str.substring(lastIndex, index);
+ }
+ lastIndex = index + 1;
+ html += escaped;
+ }
+ return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
+ }
+ var commentStripRE = /^-?>||--!>| looseEqual(item, val));
+ }
+ var toDisplayString = (val) => {
+ return val == null ? "" : isObject22(val) ? JSON.stringify(val, replacer, 2) : String(val);
+ };
+ var replacer = (_key, val) => {
+ if (isMap(val)) {
+ return {
+ [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val2]) => {
+ entries[`${key} =>`] = val2;
+ return entries;
+ }, {})
+ };
+ } else if (isSet(val)) {
+ return {
+ [`Set(${val.size})`]: [...val.values()]
+ };
+ } else if (isObject22(val) && !isArray2(val) && !isPlainObject(val)) {
+ return String(val);
+ }
+ return val;
+ };
+ var babelParserDefaultPlugins = [
+ "bigInt",
+ "optionalChaining",
+ "nullishCoalescingOperator"
+ ];
+ var EMPTY_OBJ = Object.freeze({});
+ var EMPTY_ARR = Object.freeze([]);
+ var NOOP = () => {
+ };
+ var NO = () => false;
+ var onRE = /^on[^a-z]/;
+ var isOn = (key) => onRE.test(key);
+ var isModelListener = (key) => key.startsWith("onUpdate:");
+ var extend = Object.assign;
+ var remove = (arr, el) => {
+ const i = arr.indexOf(el);
+ if (i > -1) {
+ arr.splice(i, 1);
+ }
+ };
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ var hasOwn = (val, key) => hasOwnProperty.call(val, key);
+ var isArray2 = Array.isArray;
+ var isMap = (val) => toTypeString(val) === "[object Map]";
+ var isSet = (val) => toTypeString(val) === "[object Set]";
+ var isDate = (val) => val instanceof Date;
+ var isFunction2 = (val) => typeof val === "function";
+ var isString = (val) => typeof val === "string";
+ var isSymbol = (val) => typeof val === "symbol";
+ var isObject22 = (val) => val !== null && typeof val === "object";
+ var isPromise = (val) => {
+ return isObject22(val) && isFunction2(val.then) && isFunction2(val.catch);
+ };
+ var objectToString = Object.prototype.toString;
+ var toTypeString = (value) => objectToString.call(value);
+ var toRawType = (value) => {
+ return toTypeString(value).slice(8, -1);
+ };
+ var isPlainObject = (val) => toTypeString(val) === "[object Object]";
+ var isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key;
+ var isReservedProp = /* @__PURE__ */ makeMap(
+ ",key,ref,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"
+ );
+ var cacheStringFunction = (fn) => {
+ const cache = /* @__PURE__ */ Object.create(null);
+ return (str) => {
+ const hit = cache[str];
+ return hit || (cache[str] = fn(str));
+ };
+ };
+ var camelizeRE = /-(\w)/g;
+ var camelize = cacheStringFunction((str) => {
+ return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : "");
+ });
+ var hyphenateRE = /\B([A-Z])/g;
+ var hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, "-$1").toLowerCase());
+ var capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));
+ var toHandlerKey = cacheStringFunction((str) => str ? `on${capitalize(str)}` : ``);
+ var hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);
+ var invokeArrayFns = (fns, arg) => {
+ for (let i = 0; i < fns.length; i++) {
+ fns[i](arg);
+ }
+ };
+ var def = (obj, key, value) => {
+ Object.defineProperty(obj, key, {
+ configurable: true,
+ enumerable: false,
+ value
+ });
+ };
+ var toNumber = (val) => {
+ const n = parseFloat(val);
+ return isNaN(n) ? val : n;
+ };
+ var _globalThis;
+ var getGlobalThis = () => {
+ return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {});
+ };
+ exports2.EMPTY_ARR = EMPTY_ARR;
+ exports2.EMPTY_OBJ = EMPTY_OBJ;
+ exports2.NO = NO;
+ exports2.NOOP = NOOP;
+ exports2.PatchFlagNames = PatchFlagNames;
+ exports2.babelParserDefaultPlugins = babelParserDefaultPlugins;
+ exports2.camelize = camelize;
+ exports2.capitalize = capitalize;
+ exports2.def = def;
+ exports2.escapeHtml = escapeHtml;
+ exports2.escapeHtmlComment = escapeHtmlComment;
+ exports2.extend = extend;
+ exports2.generateCodeFrame = generateCodeFrame;
+ exports2.getGlobalThis = getGlobalThis;
+ exports2.hasChanged = hasChanged;
+ exports2.hasOwn = hasOwn;
+ exports2.hyphenate = hyphenate;
+ exports2.invokeArrayFns = invokeArrayFns;
+ exports2.isArray = isArray2;
+ exports2.isBooleanAttr = isBooleanAttr2;
+ exports2.isDate = isDate;
+ exports2.isFunction = isFunction2;
+ exports2.isGloballyWhitelisted = isGloballyWhitelisted;
+ exports2.isHTMLTag = isHTMLTag;
+ exports2.isIntegerKey = isIntegerKey;
+ exports2.isKnownAttr = isKnownAttr;
+ exports2.isMap = isMap;
+ exports2.isModelListener = isModelListener;
+ exports2.isNoUnitNumericStyleProp = isNoUnitNumericStyleProp;
+ exports2.isObject = isObject22;
+ exports2.isOn = isOn;
+ exports2.isPlainObject = isPlainObject;
+ exports2.isPromise = isPromise;
+ exports2.isReservedProp = isReservedProp;
+ exports2.isSSRSafeAttrName = isSSRSafeAttrName;
+ exports2.isSVGTag = isSVGTag;
+ exports2.isSet = isSet;
+ exports2.isSpecialBooleanAttr = isSpecialBooleanAttr;
+ exports2.isString = isString;
+ exports2.isSymbol = isSymbol;
+ exports2.isVoidTag = isVoidTag;
+ exports2.looseEqual = looseEqual;
+ exports2.looseIndexOf = looseIndexOf;
+ exports2.makeMap = makeMap;
+ exports2.normalizeClass = normalizeClass;
+ exports2.normalizeStyle = normalizeStyle;
+ exports2.objectToString = objectToString;
+ exports2.parseStringStyle = parseStringStyle;
+ exports2.propsToAttrMap = propsToAttrMap;
+ exports2.remove = remove;
+ exports2.slotFlagsText = slotFlagsText;
+ exports2.stringifyStyle = stringifyStyle;
+ exports2.toDisplayString = toDisplayString;
+ exports2.toHandlerKey = toHandlerKey;
+ exports2.toNumber = toNumber;
+ exports2.toRawType = toRawType;
+ exports2.toTypeString = toTypeString;
+ }
+ });
+ var require_shared = __commonJS2({
+ "node_modules/@vue/shared/index.js"(exports2, module2) {
+ "use strict";
+ if (false) {
+ module2.exports = null;
+ } else {
+ module2.exports = require_shared_cjs();
+ }
+ }
+ });
+ var require_reactivity_cjs = __commonJS2({
+ "node_modules/@vue/reactivity/dist/reactivity.cjs.js"(exports2) {
+ "use strict";
+ Object.defineProperty(exports2, "__esModule", { value: true });
+ var shared = require_shared();
+ var targetMap = /* @__PURE__ */ new WeakMap();
+ var effectStack = [];
+ var activeEffect;
+ var ITERATE_KEY = Symbol("iterate");
+ var MAP_KEY_ITERATE_KEY = Symbol("Map key iterate");
+ function isEffect(fn) {
+ return fn && fn._isEffect === true;
+ }
+ function effect3(fn, options = shared.EMPTY_OBJ) {
+ if (isEffect(fn)) {
+ fn = fn.raw;
+ }
+ const effect4 = createReactiveEffect(fn, options);
+ if (!options.lazy) {
+ effect4();
+ }
+ return effect4;
+ }
+ function stop2(effect4) {
+ if (effect4.active) {
+ cleanup(effect4);
+ if (effect4.options.onStop) {
+ effect4.options.onStop();
+ }
+ effect4.active = false;
+ }
+ }
+ var uid = 0;
+ function createReactiveEffect(fn, options) {
+ const effect4 = function reactiveEffect() {
+ if (!effect4.active) {
+ return fn();
+ }
+ if (!effectStack.includes(effect4)) {
+ cleanup(effect4);
+ try {
+ enableTracking();
+ effectStack.push(effect4);
+ activeEffect = effect4;
+ return fn();
+ } finally {
+ effectStack.pop();
+ resetTracking();
+ activeEffect = effectStack[effectStack.length - 1];
+ }
+ }
+ };
+ effect4.id = uid++;
+ effect4.allowRecurse = !!options.allowRecurse;
+ effect4._isEffect = true;
+ effect4.active = true;
+ effect4.raw = fn;
+ effect4.deps = [];
+ effect4.options = options;
+ return effect4;
+ }
+ function cleanup(effect4) {
+ const { deps } = effect4;
+ if (deps.length) {
+ for (let i = 0; i < deps.length; i++) {
+ deps[i].delete(effect4);
+ }
+ deps.length = 0;
+ }
+ }
+ var shouldTrack = true;
+ var trackStack = [];
+ function pauseTracking() {
+ trackStack.push(shouldTrack);
+ shouldTrack = false;
+ }
+ function enableTracking() {
+ trackStack.push(shouldTrack);
+ shouldTrack = true;
+ }
+ function resetTracking() {
+ const last = trackStack.pop();
+ shouldTrack = last === void 0 ? true : last;
+ }
+ function track2(target, type, key) {
+ if (!shouldTrack || activeEffect === void 0) {
+ return;
+ }
+ let depsMap = targetMap.get(target);
+ if (!depsMap) {
+ targetMap.set(target, depsMap = /* @__PURE__ */ new Map());
+ }
+ let dep = depsMap.get(key);
+ if (!dep) {
+ depsMap.set(key, dep = /* @__PURE__ */ new Set());
+ }
+ if (!dep.has(activeEffect)) {
+ dep.add(activeEffect);
+ activeEffect.deps.push(dep);
+ if (activeEffect.options.onTrack) {
+ activeEffect.options.onTrack({
+ effect: activeEffect,
+ target,
+ type,
+ key
+ });
+ }
+ }
+ }
+ function trigger2(target, type, key, newValue, oldValue, oldTarget) {
+ const depsMap = targetMap.get(target);
+ if (!depsMap) {
+ return;
+ }
+ const effects = /* @__PURE__ */ new Set();
+ const add2 = (effectsToAdd) => {
+ if (effectsToAdd) {
+ effectsToAdd.forEach((effect4) => {
+ if (effect4 !== activeEffect || effect4.allowRecurse) {
+ effects.add(effect4);
+ }
+ });
+ }
+ };
+ if (type === "clear") {
+ depsMap.forEach(add2);
+ } else if (key === "length" && shared.isArray(target)) {
+ depsMap.forEach((dep, key2) => {
+ if (key2 === "length" || key2 >= newValue) {
+ add2(dep);
+ }
+ });
+ } else {
+ if (key !== void 0) {
+ add2(depsMap.get(key));
+ }
+ switch (type) {
+ case "add":
+ if (!shared.isArray(target)) {
+ add2(depsMap.get(ITERATE_KEY));
+ if (shared.isMap(target)) {
+ add2(depsMap.get(MAP_KEY_ITERATE_KEY));
+ }
+ } else if (shared.isIntegerKey(key)) {
+ add2(depsMap.get("length"));
+ }
+ break;
+ case "delete":
+ if (!shared.isArray(target)) {
+ add2(depsMap.get(ITERATE_KEY));
+ if (shared.isMap(target)) {
+ add2(depsMap.get(MAP_KEY_ITERATE_KEY));
+ }
+ }
+ break;
+ case "set":
+ if (shared.isMap(target)) {
+ add2(depsMap.get(ITERATE_KEY));
+ }
+ break;
+ }
+ }
+ const run = (effect4) => {
+ if (effect4.options.onTrigger) {
+ effect4.options.onTrigger({
+ effect: effect4,
+ target,
+ key,
+ type,
+ newValue,
+ oldValue,
+ oldTarget
+ });
+ }
+ if (effect4.options.scheduler) {
+ effect4.options.scheduler(effect4);
+ } else {
+ effect4();
+ }
+ };
+ effects.forEach(run);
+ }
+ var isNonTrackableKeys = /* @__PURE__ */ shared.makeMap(`__proto__,__v_isRef,__isVue`);
+ var builtInSymbols = new Set(Object.getOwnPropertyNames(Symbol).map((key) => Symbol[key]).filter(shared.isSymbol));
+ var get2 = /* @__PURE__ */ createGetter();
+ var shallowGet = /* @__PURE__ */ createGetter(false, true);
+ var readonlyGet = /* @__PURE__ */ createGetter(true);
+ var shallowReadonlyGet = /* @__PURE__ */ createGetter(true, true);
+ var arrayInstrumentations = /* @__PURE__ */ createArrayInstrumentations();
+ function createArrayInstrumentations() {
+ const instrumentations = {};
+ ["includes", "indexOf", "lastIndexOf"].forEach((key) => {
+ instrumentations[key] = function(...args) {
+ const arr = toRaw2(this);
+ for (let i = 0, l = this.length; i < l; i++) {
+ track2(arr, "get", i + "");
+ }
+ const res = arr[key](...args);
+ if (res === -1 || res === false) {
+ return arr[key](...args.map(toRaw2));
+ } else {
+ return res;
+ }
+ };
+ });
+ ["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
+ instrumentations[key] = function(...args) {
+ pauseTracking();
+ const res = toRaw2(this)[key].apply(this, args);
+ resetTracking();
+ return res;
+ };
+ });
+ return instrumentations;
+ }
+ function createGetter(isReadonly2 = false, shallow = false) {
+ return function get3(target, key, receiver) {
+ if (key === "__v_isReactive") {
+ return !isReadonly2;
+ } else if (key === "__v_isReadonly") {
+ return isReadonly2;
+ } else if (key === "__v_raw" && receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) {
+ return target;
+ }
+ const targetIsArray = shared.isArray(target);
+ if (!isReadonly2 && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) {
+ return Reflect.get(arrayInstrumentations, key, receiver);
+ }
+ const res = Reflect.get(target, key, receiver);
+ if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
+ return res;
+ }
+ if (!isReadonly2) {
+ track2(target, "get", key);
+ }
+ if (shallow) {
+ return res;
+ }
+ if (isRef(res)) {
+ const shouldUnwrap = !targetIsArray || !shared.isIntegerKey(key);
+ return shouldUnwrap ? res.value : res;
+ }
+ if (shared.isObject(res)) {
+ return isReadonly2 ? readonly(res) : reactive3(res);
+ }
+ return res;
+ };
+ }
+ var set2 = /* @__PURE__ */ createSetter();
+ var shallowSet = /* @__PURE__ */ createSetter(true);
+ function createSetter(shallow = false) {
+ return function set3(target, key, value, receiver) {
+ let oldValue = target[key];
+ if (!shallow) {
+ value = toRaw2(value);
+ oldValue = toRaw2(oldValue);
+ if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) {
+ oldValue.value = value;
+ return true;
+ }
+ }
+ const hadKey = shared.isArray(target) && shared.isIntegerKey(key) ? Number(key) < target.length : shared.hasOwn(target, key);
+ const result = Reflect.set(target, key, value, receiver);
+ if (target === toRaw2(receiver)) {
+ if (!hadKey) {
+ trigger2(target, "add", key, value);
+ } else if (shared.hasChanged(value, oldValue)) {
+ trigger2(target, "set", key, value, oldValue);
+ }
+ }
+ return result;
+ };
+ }
+ function deleteProperty(target, key) {
+ const hadKey = shared.hasOwn(target, key);
+ const oldValue = target[key];
+ const result = Reflect.deleteProperty(target, key);
+ if (result && hadKey) {
+ trigger2(target, "delete", key, void 0, oldValue);
+ }
+ return result;
+ }
+ function has(target, key) {
+ const result = Reflect.has(target, key);
+ if (!shared.isSymbol(key) || !builtInSymbols.has(key)) {
+ track2(target, "has", key);
+ }
+ return result;
+ }
+ function ownKeys(target) {
+ track2(target, "iterate", shared.isArray(target) ? "length" : ITERATE_KEY);
+ return Reflect.ownKeys(target);
+ }
+ var mutableHandlers = {
+ get: get2,
+ set: set2,
+ deleteProperty,
+ has,
+ ownKeys
+ };
+ var readonlyHandlers = {
+ get: readonlyGet,
+ set(target, key) {
+ {
+ console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
+ }
+ return true;
+ },
+ deleteProperty(target, key) {
+ {
+ console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target);
+ }
+ return true;
+ }
+ };
+ var shallowReactiveHandlers = /* @__PURE__ */ shared.extend({}, mutableHandlers, {
+ get: shallowGet,
+ set: shallowSet
+ });
+ var shallowReadonlyHandlers = /* @__PURE__ */ shared.extend({}, readonlyHandlers, {
+ get: shallowReadonlyGet
+ });
+ var toReactive = (value) => shared.isObject(value) ? reactive3(value) : value;
+ var toReadonly = (value) => shared.isObject(value) ? readonly(value) : value;
+ var toShallow = (value) => value;
+ var getProto = (v) => Reflect.getPrototypeOf(v);
+ function get$1(target, key, isReadonly2 = false, isShallow = false) {
+ target = target["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const rawKey = toRaw2(key);
+ if (key !== rawKey) {
+ !isReadonly2 && track2(rawTarget, "get", key);
+ }
+ !isReadonly2 && track2(rawTarget, "get", rawKey);
+ const { has: has2 } = getProto(rawTarget);
+ const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;
+ if (has2.call(rawTarget, key)) {
+ return wrap(target.get(key));
+ } else if (has2.call(rawTarget, rawKey)) {
+ return wrap(target.get(rawKey));
+ } else if (target !== rawTarget) {
+ target.get(key);
+ }
+ }
+ function has$1(key, isReadonly2 = false) {
+ const target = this["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const rawKey = toRaw2(key);
+ if (key !== rawKey) {
+ !isReadonly2 && track2(rawTarget, "has", key);
+ }
+ !isReadonly2 && track2(rawTarget, "has", rawKey);
+ return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey);
+ }
+ function size(target, isReadonly2 = false) {
+ target = target["__v_raw"];
+ !isReadonly2 && track2(toRaw2(target), "iterate", ITERATE_KEY);
+ return Reflect.get(target, "size", target);
+ }
+ function add(value) {
+ value = toRaw2(value);
+ const target = toRaw2(this);
+ const proto = getProto(target);
+ const hadKey = proto.has.call(target, value);
+ if (!hadKey) {
+ target.add(value);
+ trigger2(target, "add", value, value);
+ }
+ return this;
+ }
+ function set$1(key, value) {
+ value = toRaw2(value);
+ const target = toRaw2(this);
+ const { has: has2, get: get3 } = getProto(target);
+ let hadKey = has2.call(target, key);
+ if (!hadKey) {
+ key = toRaw2(key);
+ hadKey = has2.call(target, key);
+ } else {
+ checkIdentityKeys(target, has2, key);
+ }
+ const oldValue = get3.call(target, key);
+ target.set(key, value);
+ if (!hadKey) {
+ trigger2(target, "add", key, value);
+ } else if (shared.hasChanged(value, oldValue)) {
+ trigger2(target, "set", key, value, oldValue);
+ }
+ return this;
+ }
+ function deleteEntry(key) {
+ const target = toRaw2(this);
+ const { has: has2, get: get3 } = getProto(target);
+ let hadKey = has2.call(target, key);
+ if (!hadKey) {
+ key = toRaw2(key);
+ hadKey = has2.call(target, key);
+ } else {
+ checkIdentityKeys(target, has2, key);
+ }
+ const oldValue = get3 ? get3.call(target, key) : void 0;
+ const result = target.delete(key);
+ if (hadKey) {
+ trigger2(target, "delete", key, void 0, oldValue);
+ }
+ return result;
+ }
+ function clear() {
+ const target = toRaw2(this);
+ const hadItems = target.size !== 0;
+ const oldTarget = shared.isMap(target) ? new Map(target) : new Set(target);
+ const result = target.clear();
+ if (hadItems) {
+ trigger2(target, "clear", void 0, void 0, oldTarget);
+ }
+ return result;
+ }
+ function createForEach(isReadonly2, isShallow) {
+ return function forEach(callback, thisArg) {
+ const observed = this;
+ const target = observed["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;
+ !isReadonly2 && track2(rawTarget, "iterate", ITERATE_KEY);
+ return target.forEach((value, key) => {
+ return callback.call(thisArg, wrap(value), wrap(key), observed);
+ });
+ };
+ }
+ function createIterableMethod(method, isReadonly2, isShallow) {
+ return function(...args) {
+ const target = this["__v_raw"];
+ const rawTarget = toRaw2(target);
+ const targetIsMap = shared.isMap(rawTarget);
+ const isPair = method === "entries" || method === Symbol.iterator && targetIsMap;
+ const isKeyOnly = method === "keys" && targetIsMap;
+ const innerIterator = target[method](...args);
+ const wrap = isShallow ? toShallow : isReadonly2 ? toReadonly : toReactive;
+ !isReadonly2 && track2(rawTarget, "iterate", isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY);
+ return {
+ next() {
+ const { value, done } = innerIterator.next();
+ return done ? { value, done } : {
+ value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
+ done
+ };
+ },
+ [Symbol.iterator]() {
+ return this;
+ }
+ };
+ };
+ }
+ function createReadonlyMethod(type) {
+ return function(...args) {
+ {
+ const key = args[0] ? `on key "${args[0]}" ` : ``;
+ console.warn(`${shared.capitalize(type)} operation ${key}failed: target is readonly.`, toRaw2(this));
+ }
+ return type === "delete" ? false : this;
+ };
+ }
+ function createInstrumentations() {
+ const mutableInstrumentations2 = {
+ get(key) {
+ return get$1(this, key);
+ },
+ get size() {
+ return size(this);
+ },
+ has: has$1,
+ add,
+ set: set$1,
+ delete: deleteEntry,
+ clear,
+ forEach: createForEach(false, false)
+ };
+ const shallowInstrumentations2 = {
+ get(key) {
+ return get$1(this, key, false, true);
+ },
+ get size() {
+ return size(this);
+ },
+ has: has$1,
+ add,
+ set: set$1,
+ delete: deleteEntry,
+ clear,
+ forEach: createForEach(false, true)
+ };
+ const readonlyInstrumentations2 = {
+ get(key) {
+ return get$1(this, key, true);
+ },
+ get size() {
+ return size(this, true);
+ },
+ has(key) {
+ return has$1.call(this, key, true);
+ },
+ add: createReadonlyMethod(
+ "add"
+ ),
+ set: createReadonlyMethod(
+ "set"
+ ),
+ delete: createReadonlyMethod(
+ "delete"
+ ),
+ clear: createReadonlyMethod(
+ "clear"
+ ),
+ forEach: createForEach(true, false)
+ };
+ const shallowReadonlyInstrumentations2 = {
+ get(key) {
+ return get$1(this, key, true, true);
+ },
+ get size() {
+ return size(this, true);
+ },
+ has(key) {
+ return has$1.call(this, key, true);
+ },
+ add: createReadonlyMethod(
+ "add"
+ ),
+ set: createReadonlyMethod(
+ "set"
+ ),
+ delete: createReadonlyMethod(
+ "delete"
+ ),
+ clear: createReadonlyMethod(
+ "clear"
+ ),
+ forEach: createForEach(true, true)
+ };
+ const iteratorMethods = ["keys", "values", "entries", Symbol.iterator];
+ iteratorMethods.forEach((method) => {
+ mutableInstrumentations2[method] = createIterableMethod(method, false, false);
+ readonlyInstrumentations2[method] = createIterableMethod(method, true, false);
+ shallowInstrumentations2[method] = createIterableMethod(method, false, true);
+ shallowReadonlyInstrumentations2[method] = createIterableMethod(method, true, true);
+ });
+ return [
+ mutableInstrumentations2,
+ readonlyInstrumentations2,
+ shallowInstrumentations2,
+ shallowReadonlyInstrumentations2
+ ];
+ }
+ var [mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations] = /* @__PURE__ */ createInstrumentations();
+ function createInstrumentationGetter(isReadonly2, shallow) {
+ const instrumentations = shallow ? isReadonly2 ? shallowReadonlyInstrumentations : shallowInstrumentations : isReadonly2 ? readonlyInstrumentations : mutableInstrumentations;
+ return (target, key, receiver) => {
+ if (key === "__v_isReactive") {
+ return !isReadonly2;
+ } else if (key === "__v_isReadonly") {
+ return isReadonly2;
+ } else if (key === "__v_raw") {
+ return target;
+ }
+ return Reflect.get(shared.hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver);
+ };
+ }
+ var mutableCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(false, false)
+ };
+ var shallowCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(false, true)
+ };
+ var readonlyCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(true, false)
+ };
+ var shallowReadonlyCollectionHandlers = {
+ get: /* @__PURE__ */ createInstrumentationGetter(true, true)
+ };
+ function checkIdentityKeys(target, has2, key) {
+ const rawKey = toRaw2(key);
+ if (rawKey !== key && has2.call(target, rawKey)) {
+ const type = shared.toRawType(target);
+ console.warn(`Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`);
+ }
+ }
+ var reactiveMap = /* @__PURE__ */ new WeakMap();
+ var shallowReactiveMap = /* @__PURE__ */ new WeakMap();
+ var readonlyMap = /* @__PURE__ */ new WeakMap();
+ var shallowReadonlyMap = /* @__PURE__ */ new WeakMap();
+ function targetTypeMap(rawType) {
+ switch (rawType) {
+ case "Object":
+ case "Array":
+ return 1;
+ case "Map":
+ case "Set":
+ case "WeakMap":
+ case "WeakSet":
+ return 2;
+ default:
+ return 0;
+ }
+ }
+ function getTargetType(value) {
+ return value["__v_skip"] || !Object.isExtensible(value) ? 0 : targetTypeMap(shared.toRawType(value));
+ }
+ function reactive3(target) {
+ if (target && target["__v_isReadonly"]) {
+ return target;
+ }
+ return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
+ }
+ function shallowReactive(target) {
+ return createReactiveObject(target, false, shallowReactiveHandlers, shallowCollectionHandlers, shallowReactiveMap);
+ }
+ function readonly(target) {
+ return createReactiveObject(target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap);
+ }
+ function shallowReadonly(target) {
+ return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap);
+ }
+ function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {
+ if (!shared.isObject(target)) {
+ {
+ console.warn(`value cannot be made reactive: ${String(target)}`);
+ }
+ return target;
+ }
+ if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) {
+ return target;
+ }
+ const existingProxy = proxyMap.get(target);
+ if (existingProxy) {
+ return existingProxy;
+ }
+ const targetType = getTargetType(target);
+ if (targetType === 0) {
+ return target;
+ }
+ const proxy = new Proxy(target, targetType === 2 ? collectionHandlers : baseHandlers);
+ proxyMap.set(target, proxy);
+ return proxy;
+ }
+ function isReactive2(value) {
+ if (isReadonly(value)) {
+ return isReactive2(value["__v_raw"]);
+ }
+ return !!(value && value["__v_isReactive"]);
+ }
+ function isReadonly(value) {
+ return !!(value && value["__v_isReadonly"]);
+ }
+ function isProxy(value) {
+ return isReactive2(value) || isReadonly(value);
+ }
+ function toRaw2(observed) {
+ return observed && toRaw2(observed["__v_raw"]) || observed;
+ }
+ function markRaw(value) {
+ shared.def(value, "__v_skip", true);
+ return value;
+ }
+ var convert = (val) => shared.isObject(val) ? reactive3(val) : val;
+ function isRef(r) {
+ return Boolean(r && r.__v_isRef === true);
+ }
+ function ref(value) {
+ return createRef(value);
+ }
+ function shallowRef(value) {
+ return createRef(value, true);
+ }
+ var RefImpl = class {
+ constructor(value, _shallow = false) {
+ this._shallow = _shallow;
+ this.__v_isRef = true;
+ this._rawValue = _shallow ? value : toRaw2(value);
+ this._value = _shallow ? value : convert(value);
+ }
+ get value() {
+ track2(toRaw2(this), "get", "value");
+ return this._value;
+ }
+ set value(newVal) {
+ newVal = this._shallow ? newVal : toRaw2(newVal);
+ if (shared.hasChanged(newVal, this._rawValue)) {
+ this._rawValue = newVal;
+ this._value = this._shallow ? newVal : convert(newVal);
+ trigger2(toRaw2(this), "set", "value", newVal);
+ }
+ }
+ };
+ function createRef(rawValue, shallow = false) {
+ if (isRef(rawValue)) {
+ return rawValue;
+ }
+ return new RefImpl(rawValue, shallow);
+ }
+ function triggerRef(ref2) {
+ trigger2(toRaw2(ref2), "set", "value", ref2.value);
+ }
+ function unref(ref2) {
+ return isRef(ref2) ? ref2.value : ref2;
+ }
+ var shallowUnwrapHandlers = {
+ get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
+ set: (target, key, value, receiver) => {
+ const oldValue = target[key];
+ if (isRef(oldValue) && !isRef(value)) {
+ oldValue.value = value;
+ return true;
+ } else {
+ return Reflect.set(target, key, value, receiver);
+ }
+ }
+ };
+ function proxyRefs(objectWithRefs) {
+ return isReactive2(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers);
+ }
+ var CustomRefImpl = class {
+ constructor(factory) {
+ this.__v_isRef = true;
+ const { get: get3, set: set3 } = factory(() => track2(this, "get", "value"), () => trigger2(this, "set", "value"));
+ this._get = get3;
+ this._set = set3;
+ }
+ get value() {
+ return this._get();
+ }
+ set value(newVal) {
+ this._set(newVal);
+ }
+ };
+ function customRef(factory) {
+ return new CustomRefImpl(factory);
+ }
+ function toRefs(object) {
+ if (!isProxy(object)) {
+ console.warn(`toRefs() expects a reactive object but received a plain one.`);
+ }
+ const ret = shared.isArray(object) ? new Array(object.length) : {};
+ for (const key in object) {
+ ret[key] = toRef(object, key);
+ }
+ return ret;
+ }
+ var ObjectRefImpl = class {
+ constructor(_object, _key) {
+ this._object = _object;
+ this._key = _key;
+ this.__v_isRef = true;
+ }
+ get value() {
+ return this._object[this._key];
+ }
+ set value(newVal) {
+ this._object[this._key] = newVal;
+ }
+ };
+ function toRef(object, key) {
+ return isRef(object[key]) ? object[key] : new ObjectRefImpl(object, key);
+ }
+ var ComputedRefImpl = class {
+ constructor(getter, _setter, isReadonly2) {
+ this._setter = _setter;
+ this._dirty = true;
+ this.__v_isRef = true;
+ this.effect = effect3(getter, {
+ lazy: true,
+ scheduler: () => {
+ if (!this._dirty) {
+ this._dirty = true;
+ trigger2(toRaw2(this), "set", "value");
+ }
+ }
+ });
+ this["__v_isReadonly"] = isReadonly2;
+ }
+ get value() {
+ const self2 = toRaw2(this);
+ if (self2._dirty) {
+ self2._value = this.effect();
+ self2._dirty = false;
+ }
+ track2(self2, "get", "value");
+ return self2._value;
+ }
+ set value(newValue) {
+ this._setter(newValue);
+ }
+ };
+ function computed(getterOrOptions) {
+ let getter;
+ let setter;
+ if (shared.isFunction(getterOrOptions)) {
+ getter = getterOrOptions;
+ setter = () => {
+ console.warn("Write operation failed: computed value is readonly");
+ };
+ } else {
+ getter = getterOrOptions.get;
+ setter = getterOrOptions.set;
+ }
+ return new ComputedRefImpl(getter, setter, shared.isFunction(getterOrOptions) || !getterOrOptions.set);
+ }
+ exports2.ITERATE_KEY = ITERATE_KEY;
+ exports2.computed = computed;
+ exports2.customRef = customRef;
+ exports2.effect = effect3;
+ exports2.enableTracking = enableTracking;
+ exports2.isProxy = isProxy;
+ exports2.isReactive = isReactive2;
+ exports2.isReadonly = isReadonly;
+ exports2.isRef = isRef;
+ exports2.markRaw = markRaw;
+ exports2.pauseTracking = pauseTracking;
+ exports2.proxyRefs = proxyRefs;
+ exports2.reactive = reactive3;
+ exports2.readonly = readonly;
+ exports2.ref = ref;
+ exports2.resetTracking = resetTracking;
+ exports2.shallowReactive = shallowReactive;
+ exports2.shallowReadonly = shallowReadonly;
+ exports2.shallowRef = shallowRef;
+ exports2.stop = stop2;
+ exports2.toRaw = toRaw2;
+ exports2.toRef = toRef;
+ exports2.toRefs = toRefs;
+ exports2.track = track2;
+ exports2.trigger = trigger2;
+ exports2.triggerRef = triggerRef;
+ exports2.unref = unref;
+ }
+ });
+ var require_reactivity = __commonJS2({
+ "node_modules/@vue/reactivity/index.js"(exports2, module2) {
+ "use strict";
+ if (false) {
+ module2.exports = null;
+ } else {
+ module2.exports = require_reactivity_cjs();
+ }
+ }
+ });
+ var module_exports = {};
+ __export(module_exports, {
+ Alpine: () => src_default2,
+ default: () => module_default2
+ });
+ module.exports = __toCommonJS(module_exports);
+ var flushPending = false;
+ var flushing = false;
+ var queue = [];
+ var lastFlushedIndex = -1;
+ var transactionActive = false;
+ function scheduler(callback) {
+ queueJob(callback);
+ }
+ function startTransaction() {
+ transactionActive = true;
+ }
+ function commitTransaction() {
+ transactionActive = false;
+ queueFlush();
+ }
+ function queueJob(job) {
+ if (!queue.includes(job))
+ queue.push(job);
+ queueFlush();
+ }
+ function dequeueJob(job) {
+ let index = queue.indexOf(job);
+ if (index !== -1 && index > lastFlushedIndex)
+ queue.splice(index, 1);
+ }
+ function queueFlush() {
+ if (!flushing && !flushPending) {
+ if (transactionActive)
+ return;
+ flushPending = true;
+ queueMicrotask(flushJobs);
+ }
+ }
+ function flushJobs() {
+ flushPending = false;
+ flushing = true;
+ for (let i = 0; i < queue.length; i++) {
+ queue[i]();
+ lastFlushedIndex = i;
+ }
+ queue.length = 0;
+ lastFlushedIndex = -1;
+ flushing = false;
+ }
+ var reactive;
+ var effect;
+ var release;
+ var raw;
+ var shouldSchedule = true;
+ function disableEffectScheduling(callback) {
+ shouldSchedule = false;
+ callback();
+ shouldSchedule = true;
+ }
+ function setReactivityEngine(engine) {
+ reactive = engine.reactive;
+ release = engine.release;
+ effect = (callback) => engine.effect(callback, { scheduler: (task) => {
+ if (shouldSchedule) {
+ scheduler(task);
+ } else {
+ task();
+ }
+ } });
+ raw = engine.raw;
+ }
+ function overrideEffect(override) {
+ effect = override;
+ }
+ function elementBoundEffect(el) {
+ let cleanup = () => {
+ };
+ let wrappedEffect = (callback) => {
+ let effectReference = effect(callback);
+ if (!el._x_effects) {
+ el._x_effects = /* @__PURE__ */ new Set();
+ el._x_runEffects = () => {
+ el._x_effects.forEach((i) => i());
+ };
+ }
+ el._x_effects.add(effectReference);
+ cleanup = () => {
+ if (effectReference === void 0)
+ return;
+ el._x_effects.delete(effectReference);
+ release(effectReference);
+ };
+ return effectReference;
+ };
+ return [wrappedEffect, () => {
+ cleanup();
+ }];
+ }
+ function watch(getter, callback) {
+ let firstTime = true;
+ let oldValue;
+ let oldValueJSON;
+ let effectReference = effect(() => {
+ let value = getter();
+ let newJSON = JSON.stringify(value);
+ if (!firstTime) {
+ if (typeof value === "object" || value !== oldValue) {
+ let previousValue = typeof oldValue === "object" ? JSON.parse(oldValueJSON) : oldValue;
+ queueMicrotask(() => {
+ callback(value, previousValue);
+ });
+ }
+ }
+ oldValue = value;
+ oldValueJSON = newJSON;
+ firstTime = false;
+ });
+ return () => release(effectReference);
+ }
+ async function transaction(callback) {
+ startTransaction();
+ try {
+ await callback();
+ await Promise.resolve();
+ } finally {
+ commitTransaction();
+ }
+ }
+ var onAttributeAddeds = [];
+ var onElRemoveds = [];
+ var onElAddeds = [];
+ function onElAdded(callback) {
+ onElAddeds.push(callback);
+ }
+ function onElRemoved(el, callback) {
+ if (typeof callback === "function") {
+ if (!el._x_cleanups)
+ el._x_cleanups = [];
+ el._x_cleanups.push(callback);
+ } else {
+ callback = el;
+ onElRemoveds.push(callback);
+ }
+ }
+ function onAttributesAdded(callback) {
+ onAttributeAddeds.push(callback);
+ }
+ function onAttributeRemoved(el, name, callback) {
+ if (!el._x_attributeCleanups)
+ el._x_attributeCleanups = {};
+ if (!el._x_attributeCleanups[name])
+ el._x_attributeCleanups[name] = [];
+ el._x_attributeCleanups[name].push(callback);
+ }
+ function cleanupAttributes(el, names) {
+ if (!el._x_attributeCleanups)
+ return;
+ Object.entries(el._x_attributeCleanups).forEach(([name, value]) => {
+ if (names === void 0 || names.includes(name)) {
+ value.forEach((i) => i());
+ delete el._x_attributeCleanups[name];
+ }
+ });
+ }
+ function cleanupElement(el) {
+ var _a, _b;
+ (_a = el._x_effects) == null ? void 0 : _a.forEach(dequeueJob);
+ while ((_b = el._x_cleanups) == null ? void 0 : _b.length)
+ el._x_cleanups.pop()();
+ }
+ var observer = new MutationObserver(onMutate);
+ var currentlyObserving = false;
+ function startObservingMutations() {
+ observer.observe(document, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
+ currentlyObserving = true;
+ }
+ function stopObservingMutations() {
+ flushObserver();
+ observer.disconnect();
+ currentlyObserving = false;
+ }
+ var queuedMutations = [];
+ function flushObserver() {
+ let records = observer.takeRecords();
+ queuedMutations.push(() => records.length > 0 && onMutate(records));
+ let queueLengthWhenTriggered = queuedMutations.length;
+ queueMicrotask(() => {
+ if (queuedMutations.length === queueLengthWhenTriggered) {
+ while (queuedMutations.length > 0)
+ queuedMutations.shift()();
+ }
+ });
+ }
+ function mutateDom(callback) {
+ if (!currentlyObserving)
+ return callback();
+ stopObservingMutations();
+ let result = callback();
+ startObservingMutations();
+ return result;
+ }
+ var isCollecting = false;
+ var deferredMutations = [];
+ function deferMutations() {
+ isCollecting = true;
+ }
+ function flushAndStopDeferringMutations() {
+ isCollecting = false;
+ onMutate(deferredMutations);
+ deferredMutations = [];
+ }
+ function onMutate(mutations) {
+ if (isCollecting) {
+ deferredMutations = deferredMutations.concat(mutations);
+ return;
+ }
+ let addedNodes = [];
+ let removedNodes = /* @__PURE__ */ new Set();
+ let addedAttributes = /* @__PURE__ */ new Map();
+ let removedAttributes = /* @__PURE__ */ new Map();
+ for (let i = 0; i < mutations.length; i++) {
+ if (mutations[i].target._x_ignoreMutationObserver)
+ continue;
+ if (mutations[i].type === "childList") {
+ mutations[i].removedNodes.forEach((node) => {
+ if (node.nodeType !== 1)
+ return;
+ if (!node._x_marker)
+ return;
+ removedNodes.add(node);
+ });
+ mutations[i].addedNodes.forEach((node) => {
+ if (node.nodeType !== 1)
+ return;
+ if (removedNodes.has(node)) {
+ removedNodes.delete(node);
+ return;
+ }
+ if (node._x_marker)
+ return;
+ addedNodes.push(node);
+ });
+ }
+ if (mutations[i].type === "attributes") {
+ let el = mutations[i].target;
+ let name = mutations[i].attributeName;
+ let oldValue = mutations[i].oldValue;
+ let add = () => {
+ if (!addedAttributes.has(el))
+ addedAttributes.set(el, []);
+ addedAttributes.get(el).push({ name, value: el.getAttribute(name) });
+ };
+ let remove = () => {
+ if (!removedAttributes.has(el))
+ removedAttributes.set(el, []);
+ removedAttributes.get(el).push(name);
+ };
+ if (el.hasAttribute(name) && oldValue === null) {
+ add();
+ } else if (el.hasAttribute(name)) {
+ remove();
+ add();
+ } else {
+ remove();
+ }
+ }
+ }
+ removedAttributes.forEach((attrs, el) => {
+ cleanupAttributes(el, attrs);
+ });
+ addedAttributes.forEach((attrs, el) => {
+ onAttributeAddeds.forEach((i) => i(el, attrs));
+ });
+ for (let node of removedNodes) {
+ if (addedNodes.some((i) => i.contains(node)))
+ continue;
+ onElRemoveds.forEach((i) => i(node));
+ }
+ for (let node of addedNodes) {
+ if (!node.isConnected)
+ continue;
+ onElAddeds.forEach((i) => i(node));
+ }
+ addedNodes = null;
+ removedNodes = null;
+ addedAttributes = null;
+ removedAttributes = null;
+ }
+ function scope(node) {
+ return mergeProxies(closestDataStack(node));
+ }
+ function addScopeToNode(node, data2, referenceNode) {
+ node._x_dataStack = [data2, ...closestDataStack(referenceNode || node)];
+ return () => {
+ node._x_dataStack = node._x_dataStack.filter((i) => i !== data2);
+ };
+ }
+ function closestDataStack(node) {
+ if (node._x_dataStack)
+ return node._x_dataStack;
+ if (typeof ShadowRoot === "function" && node instanceof ShadowRoot) {
+ return closestDataStack(node.host);
+ }
+ if (!node.parentNode) {
+ return [];
+ }
+ return closestDataStack(node.parentNode);
+ }
+ function mergeProxies(objects) {
+ return new Proxy({ objects }, mergeProxyTrap);
+ }
+ function keyInPrototypeChain(obj, key) {
+ if (obj === null || obj === Object.prototype)
+ return null;
+ if (Object.prototype.hasOwnProperty.call(obj, key))
+ return obj;
+ return keyInPrototypeChain(Object.getPrototypeOf(obj), key);
+ }
+ var mergeProxyTrap = {
+ ownKeys({ objects }) {
+ return Array.from(
+ new Set(objects.flatMap((i) => Object.keys(i)))
+ );
+ },
+ has({ objects }, name) {
+ if (name == Symbol.unscopables)
+ return false;
+ return objects.some(
+ (obj) => Object.prototype.hasOwnProperty.call(obj, name) || Reflect.has(obj, name)
+ );
+ },
+ get({ objects }, name, thisProxy) {
+ if (name == "toJSON")
+ return collapseProxies;
+ return Reflect.get(
+ objects.find(
+ (obj) => Reflect.has(obj, name)
+ ) || {},
+ name,
+ thisProxy
+ );
+ },
+ set({ objects }, name, value, thisProxy) {
+ let target;
+ for (const obj of objects) {
+ target = keyInPrototypeChain(obj, name);
+ if (target)
+ break;
+ }
+ if (!target)
+ target = objects[objects.length - 1];
+ const descriptor = Object.getOwnPropertyDescriptor(target, name);
+ if ((descriptor == null ? void 0 : descriptor.set) && (descriptor == null ? void 0 : descriptor.get))
+ return descriptor.set.call(thisProxy, value) || true;
+ return Reflect.set(target, name, value);
+ }
+ };
+ function collapseProxies() {
+ let keys = Reflect.ownKeys(this);
+ return keys.reduce((acc, key) => {
+ acc[key] = Reflect.get(this, key);
+ return acc;
+ }, {});
+ }
+ function initInterceptors(data2) {
+ let isObject22 = (val) => typeof val === "object" && !Array.isArray(val) && val !== null;
+ let recurse = (obj, basePath = "") => {
+ Object.entries(Object.getOwnPropertyDescriptors(obj)).forEach(([key, { value, enumerable }]) => {
+ if (enumerable === false || value === void 0)
+ return;
+ if (typeof value === "object" && value !== null && value.__v_skip)
+ return;
+ let path = basePath === "" ? key : `${basePath}.${key}`;
+ if (typeof value === "object" && value !== null && value._x_interceptor) {
+ obj[key] = value.initialize(data2, path, key);
+ } else {
+ if (isObject22(value) && value !== obj && !(value instanceof Element)) {
+ recurse(value, path);
+ }
+ }
+ });
+ };
+ return recurse(data2);
+ }
+ function interceptor(callback, mutateObj = () => {
+ }) {
+ let obj = {
+ initialValue: void 0,
+ _x_interceptor: true,
+ initialize(data2, path, key) {
+ return callback(this.initialValue, () => get(data2, path), (value) => set(data2, path, value), path, key);
+ }
+ };
+ mutateObj(obj);
+ return (initialValue) => {
+ if (typeof initialValue === "object" && initialValue !== null && initialValue._x_interceptor) {
+ let initialize = obj.initialize.bind(obj);
+ obj.initialize = (data2, path, key) => {
+ let innerValue = initialValue.initialize(data2, path, key);
+ obj.initialValue = innerValue;
+ return initialize(data2, path, key);
+ };
+ } else {
+ obj.initialValue = initialValue;
+ }
+ return obj;
+ };
+ }
+ function get(obj, path) {
+ return path.split(".").reduce((carry, segment) => carry[segment], obj);
+ }
+ function set(obj, path, value) {
+ if (typeof path === "string")
+ path = path.split(".");
+ if (path.length === 1)
+ obj[path[0]] = value;
+ else if (path.length === 0)
+ throw error;
+ else {
+ if (obj[path[0]])
+ return set(obj[path[0]], path.slice(1), value);
+ else {
+ obj[path[0]] = {};
+ return set(obj[path[0]], path.slice(1), value);
+ }
+ }
+ }
+ var magics = {};
+ function magic(name, callback) {
+ magics[name] = callback;
+ }
+ function injectMagics(obj, el) {
+ let memoizedUtilities = getUtilities(el);
+ Object.entries(magics).forEach(([name, callback]) => {
+ Object.defineProperty(obj, `$${name}`, {
+ get() {
+ return callback(el, memoizedUtilities);
+ },
+ enumerable: false
+ });
+ });
+ return obj;
+ }
+ function getUtilities(el) {
+ let [utilities, cleanup] = getElementBoundUtilities(el);
+ let utils = { interceptor, ...utilities };
+ onElRemoved(el, cleanup);
+ return utils;
+ }
+ function tryCatch(el, expression, callback, ...args) {
+ try {
+ return callback(...args);
+ } catch (e) {
+ handleError(e, el, expression);
+ }
+ }
+ function handleError(...args) {
+ return errorHandler(...args);
+ }
+ var errorHandler = normalErrorHandler;
+ function setErrorHandler(handler4) {
+ errorHandler = handler4;
+ }
+ function normalErrorHandler(error2, el, expression = void 0) {
+ error2 = Object.assign(
+ error2 != null ? error2 : { message: "No error message given." },
+ { el, expression }
+ );
+ console.warn(`Alpine Expression Error: ${error2.message}
+
+${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
+ setTimeout(() => {
+ throw error2;
+ }, 0);
+ }
+ var shouldAutoEvaluateFunctions = true;
+ function dontAutoEvaluateFunctions(callback) {
+ let cache = shouldAutoEvaluateFunctions;
+ shouldAutoEvaluateFunctions = false;
+ let result = callback();
+ shouldAutoEvaluateFunctions = cache;
+ return result;
+ }
+ function evaluate(el, expression, extras = {}) {
+ let result;
+ evaluateLater(el, expression)((value) => result = value, extras);
+ return result;
+ }
+ function evaluateLater(...args) {
+ return theEvaluatorFunction(...args);
+ }
+ var theEvaluatorFunction = () => {
+ };
+ function setEvaluator(newEvaluator) {
+ theEvaluatorFunction = newEvaluator;
+ }
+ var theRawEvaluatorFunction;
+ function setRawEvaluator(newEvaluator) {
+ theRawEvaluatorFunction = newEvaluator;
+ }
+ function generateEvaluatorFromFunction(dataStack, func) {
+ return (receiver = () => {
+ }, { scope: scope2 = {}, params = [], context } = {}) => {
+ if (!shouldAutoEvaluateFunctions) {
+ runIfTypeOfFunction(receiver, func, mergeProxies([scope2, ...dataStack]), params);
+ return;
+ }
+ let result = func.apply(mergeProxies([scope2, ...dataStack]), params);
+ runIfTypeOfFunction(receiver, result);
+ };
+ }
+ function runIfTypeOfFunction(receiver, value, scope2, params, el) {
+ if (shouldAutoEvaluateFunctions && typeof value === "function") {
+ let result = value.apply(scope2, params);
+ if (result instanceof Promise) {
+ result.then((i) => runIfTypeOfFunction(receiver, i, scope2, params)).catch((error2) => handleError(error2, el, value));
+ } else {
+ receiver(result);
+ }
+ } else if (typeof value === "object" && value instanceof Promise) {
+ value.then((i) => receiver(i));
+ } else {
+ receiver(value);
+ }
+ }
+ function evaluateRaw(...args) {
+ return theRawEvaluatorFunction(...args);
+ }
+ var prefixAsString = "x-";
+ function prefix(subject = "") {
+ return prefixAsString + subject;
+ }
+ function setPrefix(newPrefix) {
+ prefixAsString = newPrefix;
+ }
+ var directiveHandlers = {};
+ function directive2(name, callback) {
+ directiveHandlers[name] = callback;
+ return {
+ before(directive22) {
+ if (!directiveHandlers[directive22]) {
+ console.warn(String.raw`Cannot find directive \`${directive22}\`. \`${name}\` will use the default order of execution`);
+ return;
+ }
+ const pos = directiveOrder.indexOf(directive22);
+ directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf("DEFAULT"), 0, name);
+ }
+ };
+ }
+ function directiveExists(name) {
+ return Object.keys(directiveHandlers).includes(name);
+ }
+ function directives(el, attributes, originalAttributeOverride) {
+ attributes = Array.from(attributes);
+ if (el._x_virtualDirectives) {
+ let vAttributes = Object.entries(el._x_virtualDirectives).map(([name, value]) => ({ name, value }));
+ let staticAttributes = attributesOnly(vAttributes);
+ vAttributes = vAttributes.map((attribute) => {
+ if (staticAttributes.find((attr) => attr.name === attribute.name)) {
+ return {
+ name: `x-bind:${attribute.name}`,
+ value: `"${attribute.value}"`
+ };
+ }
+ return attribute;
+ });
+ attributes = attributes.concat(vAttributes);
+ }
+ let transformedAttributeMap = {};
+ let directives2 = attributes.map(toTransformedAttributes((newName, oldName) => transformedAttributeMap[newName] = oldName)).filter(outNonAlpineAttributes).map(toParsedDirectives(transformedAttributeMap, originalAttributeOverride)).sort(byPriority);
+ return directives2.map((directive22) => {
+ return getDirectiveHandler(el, directive22);
+ });
+ }
+ function attributesOnly(attributes) {
+ return Array.from(attributes).map(toTransformedAttributes()).filter((attr) => !outNonAlpineAttributes(attr));
+ }
+ var isDeferringHandlers = false;
+ var directiveHandlerStacks = /* @__PURE__ */ new Map();
+ var currentHandlerStackKey = Symbol();
+ function deferHandlingDirectives(callback) {
+ isDeferringHandlers = true;
+ let key = Symbol();
+ currentHandlerStackKey = key;
+ directiveHandlerStacks.set(key, []);
+ let flushHandlers = () => {
+ while (directiveHandlerStacks.get(key).length)
+ directiveHandlerStacks.get(key).shift()();
+ directiveHandlerStacks.delete(key);
+ };
+ let stopDeferring = () => {
+ isDeferringHandlers = false;
+ flushHandlers();
+ };
+ callback(flushHandlers);
+ stopDeferring();
+ }
+ function getElementBoundUtilities(el) {
+ let cleanups2 = [];
+ let cleanup = (callback) => cleanups2.push(callback);
+ let [effect3, cleanupEffect] = elementBoundEffect(el);
+ cleanups2.push(cleanupEffect);
+ let utilities = {
+ Alpine: alpine_default,
+ effect: effect3,
+ cleanup,
+ evaluateLater: evaluateLater.bind(evaluateLater, el),
+ evaluate: evaluate.bind(evaluate, el)
+ };
+ let doCleanup = () => cleanups2.forEach((i) => i());
+ return [utilities, doCleanup];
+ }
+ function getDirectiveHandler(el, directive22) {
+ let noop = () => {
+ };
+ let handler4 = directiveHandlers[directive22.type] || noop;
+ let [utilities, cleanup] = getElementBoundUtilities(el);
+ onAttributeRemoved(el, directive22.original, cleanup);
+ let fullHandler = () => {
+ if (el._x_ignore || el._x_ignoreSelf)
+ return;
+ handler4.inline && handler4.inline(el, directive22, utilities);
+ handler4 = handler4.bind(handler4, el, directive22, utilities);
+ isDeferringHandlers ? directiveHandlerStacks.get(currentHandlerStackKey).push(handler4) : handler4();
+ };
+ fullHandler.runCleanups = cleanup;
+ return fullHandler;
+ }
+ var startingWith = (subject, replacement) => ({ name, value }) => {
+ if (name.startsWith(subject))
+ name = name.replace(subject, replacement);
+ return { name, value };
+ };
+ var into = (i) => i;
+ function toTransformedAttributes(callback = () => {
+ }) {
+ return ({ name, value }) => {
+ let { name: newName, value: newValue } = attributeTransformers.reduce((carry, transform) => {
+ return transform(carry);
+ }, { name, value });
+ if (newName !== name)
+ callback(newName, name);
+ return { name: newName, value: newValue };
+ };
+ }
+ var attributeTransformers = [];
+ function mapAttributes(callback) {
+ attributeTransformers.push(callback);
+ }
+ function outNonAlpineAttributes({ name }) {
+ return alpineAttributeRegex().test(name);
+ }
+ var alpineAttributeRegex = () => new RegExp(`^${prefixAsString}([^:^.]+)\\b`);
+ function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) {
+ return ({ name, value }) => {
+ if (name === value)
+ value = "";
+ let typeMatch = name.match(alpineAttributeRegex());
+ let valueMatch = name.match(/:([a-zA-Z0-9\-_:]+)/);
+ let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
+ let original = originalAttributeOverride || transformedAttributeMap[name] || name;
+ return {
+ type: typeMatch ? typeMatch[1] : null,
+ value: valueMatch ? valueMatch[1] : null,
+ modifiers: modifiers.map((i) => i.replace(".", "")),
+ expression: value,
+ original
+ };
+ };
+ }
+ var DEFAULT = "DEFAULT";
+ var directiveOrder = [
+ "ignore",
+ "ref",
+ "id",
+ "data",
+ "anchor",
+ "bind",
+ "init",
+ "for",
+ "model",
+ "modelable",
+ "transition",
+ "show",
+ "if",
+ DEFAULT,
+ "teleport"
+ ];
+ function byPriority(a, b) {
+ let typeA = directiveOrder.indexOf(a.type) === -1 ? DEFAULT : a.type;
+ let typeB = directiveOrder.indexOf(b.type) === -1 ? DEFAULT : b.type;
+ return directiveOrder.indexOf(typeA) - directiveOrder.indexOf(typeB);
+ }
+ function dispatch3(el, name, detail = {}, options = {}) {
+ return el.dispatchEvent(
+ new CustomEvent(name, {
+ detail,
+ bubbles: true,
+ composed: true,
+ cancelable: true,
+ ...options
+ })
+ );
+ }
+ function walk(el, callback) {
+ if (typeof ShadowRoot === "function" && el instanceof ShadowRoot) {
+ Array.from(el.children).forEach((el2) => walk(el2, callback));
+ return;
+ }
+ let skip = false;
+ callback(el, () => skip = true);
+ if (skip)
+ return;
+ let node = el.firstElementChild;
+ while (node) {
+ walk(node, callback, false);
+ node = node.nextElementSibling;
+ }
+ }
+ function warn(message, ...args) {
+ console.warn(`Alpine Warning: ${message}`, ...args);
+ }
+ var started = false;
+ function start2() {
+ if (started)
+ warn("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.");
+ started = true;
+ if (!document.body)
+ warn("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `