Product Category
Category pages — a banner with a faceted filter sidebar (categories, price, swatches) and a product grid.
Preview
@props([
'eyebrow' => 'The Collection',
'title' => 'All Bags',
'subheading' => 'A considered edit of structured leather pieces — filtered to find the one that carries from one season into the next.',
'count' => null,
'categories' => null,
'materials' => null,
'colors' => null,
'price' => null,
'products' => null,
'currency' => 'EUR',
'locale' => 'en-IE',
])
{{--
product-category-04 — "Boutique" Monogram editorial faceted collection page.
A family member of product-category-filtered (the same faceted filter + grid layout)
re-cut in the editorial Monogram register: a slim header (font-heading collection
title, eyebrow, result count and a sort select), a left filter rail (category
list, min/max price range, material checkboxes and token-only colour swatches)
and a responsive grid of product cards (name, material line, price with an
optional compare-at and a sale badge; no rating, wishlist or quick-add, so the
grid keeps its calm editorial register). On small screens the rail collapses
behind an inline-Alpine "Filters" drawer; sort is the select primitive.
Composes <x-ui.product-card>, <x-ui.select>, <x-ui.color-swatch>, <x-ui.button>,
<x-ui.checkbox>, <x-ui.input>, <x-ui.separator>.
Semantic tokens only, logical spacing, WCAG AA, RTL-safe, reduced-motion.
--}}
@php
$categories ??= [
['label' => 'Totes', 'count' => 48, 'checked' => true],
['label' => 'Crossbody', 'count' => 36, 'checked' => false],
['label' => 'Top Handle', 'count' => 27, 'checked' => false],
['label' => 'Clutches', 'count' => 19, 'checked' => false],
['label' => 'Backpacks', 'count' => 22, 'checked' => false],
['label' => 'Travel', 'count' => 14, 'checked' => false],
];
$materials ??= [
['label' => 'Calf leather', 'checked' => false],
['label' => 'Pebbled grain', 'checked' => true],
['label' => 'Suede', 'checked' => false],
['label' => 'Recycled canvas','checked' => false],
];
// Colour facets use semantic token classes — never raw colours.
$colors ??= [
['label' => 'Onyx', 'class' => 'bg-foreground', 'active' => true],
['label' => 'Stone', 'class' => 'bg-muted-foreground', 'active' => false],
['label' => 'Sand', 'class' => 'bg-secondary', 'active' => false],
['label' => 'Cognac', 'class' => 'bg-accent', 'active' => false],
['label' => 'Ember', 'class' => 'bg-primary', 'active' => false],
['label' => 'Crimson', 'class' => 'bg-destructive', 'active' => false],
];
$price ??= ['min' => 120, 'max' => 980];
$products ??= [
['name' => 'Aurelia Structured Tote', 'meta' => 'Calf leather, Onyx', 'price' => '680', 'was' => null, 'badge' => null, 'badgeVariant' => 'secondary'],
['name' => 'Margaux Crossbody', 'meta' => 'Pebbled grain, Sand', 'price' => '420', 'was' => '520', 'badge' => 'Sale', 'badgeVariant' => 'destructive'],
['name' => 'Colette Top Handle', 'meta' => 'Calf leather, Cognac', 'price' => '740', 'was' => null, 'badge' => 'New', 'badgeVariant' => 'default'],
['name' => 'Esme Evening Clutch', 'meta' => 'Satin, Onyx', 'price' => '310', 'was' => null, 'badge' => null, 'badgeVariant' => 'secondary'],
['name' => 'Juno Backpack', 'meta' => 'Recycled canvas, Stone', 'price' => '360', 'was' => '440', 'badge' => 'Sale', 'badgeVariant' => 'destructive'],
['name' => 'Liv Weekender', 'meta' => 'Pebbled grain, Ember', 'price' => '980', 'was' => null, 'badge' => null, 'badgeVariant' => 'secondary'],
];
$activeColor = collect($colors)->first(fn ($color) => ! empty($color['active']))['label'] ?? null;
$count ??= count($products);
@endphp
<section
{{ $attributes->merge(['class' => 'bg-background py-14 text-foreground sm:py-20']) }}
data-slot="product-category"
aria-label="{{ __($title) }}"
x-data="{ filtersOpen: false, sort: 'featured' }"
>
<div class="mx-auto max-w-7xl px-6 sm:px-8 lg:px-12">
{{-- ── Slim collection header ── --}}
<header class="flex flex-col gap-6 border-b border-border pb-8 sm:flex-row sm:items-end sm:justify-between">
<div class="max-w-2xl">
@if ($eyebrow)
<p class="font-body text-sm uppercase tracking-[0.3em] text-muted-foreground">
{{ __($eyebrow) }}
</p>
@endif
<h1 class="mt-4 font-heading text-3xl font-normal leading-[1.1] tracking-tight text-balance sm:text-4xl lg:text-5xl">
{{ __($title) }}
</h1>
@if ($subheading)
<p class="mt-4 max-w-md font-body text-sm leading-relaxed text-muted-foreground text-pretty">
{{ __($subheading) }}
</p>
@endif
</div>
<div class="flex items-center justify-between gap-4 sm:flex-col sm:items-end">
<p class="font-body text-sm text-muted-foreground">
{{ trans_choice('{0}No pieces|{1}:count piece|[2,*]:count pieces', $count, ['count' => $count]) }}
</p>
<label class="flex items-center gap-2 font-body text-sm text-muted-foreground">
<span class="shrink-0 uppercase tracking-wide text-sm">{{ __('Sort') }}</span>
<x-ui.select size="sm" name="sort" x-model="sort" class="w-48">
<option value="featured">{{ __('Featured') }}</option>
<option value="newest">{{ __('Newest') }}</option>
<option value="price-asc">{{ __('Price: low to high') }}</option>
<option value="price-desc">{{ __('Price: high to low') }}</option>
</x-ui.select>
</label>
</div>
</header>
{{-- ── Mobile filter trigger ── --}}
<div class="mt-6 lg:hidden">
<x-ui.button
type="button"
variant="outline"
class="w-full"
x-on:click="filtersOpen = true"
x-bind:aria-expanded="filtersOpen.toString()"
aria-controls="product-category-04-filters"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 6h16M7 12h10M10 18h4" />
</svg>
{{ __('Filters') }}
</x-ui.button>
</div>
<div class="mt-8 flex flex-col gap-10 lg:flex-row">
{{-- ── Filter rail (drawer < lg, static rail ≥ lg) ── --}}
<div
class="fixed inset-0 z-40 bg-scrim/40 backdrop-blur-sm transition-opacity lg:hidden"
x-show="filtersOpen"
x-transition.opacity
x-on:click="filtersOpen = false"
x-cloak
aria-hidden="true"
></div>
<aside
id="product-category-04-filters"
class="fixed inset-y-0 start-0 z-50 w-80 max-w-[85vw] overflow-y-auto border-e border-border bg-background p-6 shadow-xl transition-transform duration-300 ease-out lg:static lg:z-auto lg:w-60 lg:max-w-none lg:shrink-0 lg:overflow-visible lg:border-e-0 lg:bg-transparent lg:p-0 lg:shadow-none rtl:shadow-xl"
x-bind:class="filtersOpen ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full lg:translate-x-0 rtl:lg:translate-x-0'"
aria-label="{{ __('Product filters') }}"
>
<div class="mb-6 flex items-center justify-between lg:hidden">
<span class="font-heading text-lg font-normal tracking-tight">{{ __('Filters') }}</span>
<x-ui.button
type="button"
variant="ghost"
size="icon"
x-on:click="filtersOpen = false"
aria-label="{{ __('Close filters') }}"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</x-ui.button>
</div>
{{-- Category --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Category') }}</legend>
<div class="flex flex-col gap-4">
@foreach ($categories as $category)
<label class="flex cursor-pointer items-center gap-2 font-body text-sm">
<x-ui.checkbox :checked="$category['checked'] ?? false" name="category[]" :value="$category['label']" />
<span class="flex-1 text-foreground">{{ __($category['label']) }}</span>
<span class="text-sm tabular-nums text-muted-foreground">{{ $category['count'] }}</span>
</label>
@endforeach
</div>
</fieldset>
<x-ui.separator class="my-6" />
{{-- Price range --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Price') }}</legend>
<div class="flex items-end gap-4">
<label class="flex-1">
<span class="mb-2 block font-body text-sm text-muted-foreground">{{ __('Min') }}</span>
<x-ui.input type="number" inputmode="numeric" min="0" name="price_min" :value="$price['min']" aria-label="{{ __('Minimum price') }}" />
</label>
<span class="pb-2 text-muted-foreground" aria-hidden="true">–</span>
<label class="flex-1">
<span class="mb-2 block font-body text-sm text-muted-foreground">{{ __('Max') }}</span>
<x-ui.input type="number" inputmode="numeric" min="0" name="price_max" :value="$price['max']" aria-label="{{ __('Maximum price') }}" />
</label>
</div>
</fieldset>
<x-ui.separator class="my-6" />
{{-- Material --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Material') }}</legend>
<div class="flex flex-col gap-4">
@foreach ($materials as $material)
<label class="flex cursor-pointer items-center gap-2 font-body text-sm">
<x-ui.checkbox :checked="$material['checked'] ?? false" name="material[]" :value="$material['label']" />
<span class="flex-1 text-foreground">{{ __($material['label']) }}</span>
</label>
@endforeach
</div>
</fieldset>
<x-ui.separator class="my-6" />
{{-- Colour --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Colour') }}</legend>
<x-ui.color-swatch name="colour" size="sm" :label="__('Colour')" :colors="$colors" :selected="$activeColor" />
</fieldset>
<x-ui.separator class="my-6" />
<x-ui.button variant="outline" size="sm" class="w-full">{{ __('Clear all') }}</x-ui.button>
</aside>
{{-- ── Product grid ── --}}
<div class="min-w-0 flex-1">
<ul role="list" class="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($products as $product)
@php
// `meta` / `was` are this block's data keys; the card reads
// `description` / `compareAt`, so map them here.
$card = [
'name' => $product['name'],
'href' => $product['href'] ?? '#',
'description' => $product['meta'] ?? $product['description'] ?? null,
'price' => $product['price'],
'compareAt' => $product['was'] ?? $product['compareAt'] ?? null,
'badge' => $product['badge'] ?? null,
'badgeVariant' => $product['badgeVariant'] ?? 'default',
'gradient' => 'from-muted to-muted',
];
@endphp
<li>
<x-ui.product-card :product="$card" :currency="$currency" :locale="$locale" heading-level="h2" :show-rating="false" :show-wishlist="false" :show-actions="false" />
</li>
@endforeach
</ul>
{{-- Load more --}}
<div class="mt-14 flex justify-center">
<x-ui.button variant="outline">{{ __('Load more') }}</x-ui.button>
</div>
</div>
</div>
</div>
</section>
Installation
php artisan ui:add blocks/product-category-boutique
Registry contract
Install confidence
php artisan ui:add blocks/product-category-boutique
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.1.2
- License
- open
- Stability
- stable
- Contract
- v1
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/product-category-boutique.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Product Category Boutique (`product-category-boutique`)
An editorial boutique collection page with a slim header (eyebrow, display title, result count and sort), a faceted filter rail (category checkboxes, min/max price range, material checkboxes and token-only colour swatches) that collapses into a mobile drawer, and a responsive grid of borderless editorial product cards with placeholder, name, price and an optional sale badge.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/product-category-boutique
```
## Render it
```blade
<x-blocks.product-category-boutique />
```
## Props
- `eyebrow` (string, default `The Collection`) — Small uppercase label shown above the title.
- `title` (string, default `All Bags`) — Collection title.
- `subheading` (string, default `A considered edit of structured leather pieces — filtered to find the one that carries from one season into the next.`) — Supporting copy under the title.
- `count` (mixed|null, default `null`) — Result count shown in the header; null derives it from the products array.
- `categories` (mixed|null, default `null`) — Array of {label, count, checked} category filter entries.
- `materials` (mixed|null, default `null`) — Array of {label, checked} material filter entries.
- `colors` (mixed|null, default `null`) — Array of {label, class, active} colour swatch filter entries using semantic token classes.
- `price` (mixed|null, default `null`) — Min/max price-range bounds, e.g. {min, max}.
- `products` (mixed|null, default `null`) — Array of product entries rendered in the grid.
- `currency` (string, default `EUR`) — ISO currency code used to format product prices.
- `locale` (string, default `en-IE`) — Locale used for number/price formatting.
## Use when
- Building a full editorial "Boutique" collection page in one block: a slim header (title, eyebrow, result count, sort select) beside a faceted filter rail of category, price-range, material and colour facets, and a borderless product grid without ratings or wishlist.
- Needing the filter rail to collapse into a real mobile drawer, opened and closed with Alpine state, rather than a static sidebar.
## Avoid when
- Needing star ratings, wishlist icons and bestseller/sale badge styling in the standard commerce register; use product-category-filtered instead.
## Rules
- Render the installed block with `<x-blocks.product-category-boutique />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/product-category-boutique
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/product-category-boutique.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Building a full editorial "Boutique" collection page in one block: a slim header (title, eyebrow, result count, sort select) beside a faceted filter rail of category, price-range, material and colour facets, and a borderless product grid without ratings or wishlist.
- Needing the filter rail to collapse into a real mobile drawer, opened and closed with Alpine state, rather than a static sidebar.
Avoid when
- Needing star ratings, wishlist icons and bestseller/sale badge styling in the standard commerce register; use product-category-filtered instead.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
Usage
Render the block as a component, passing data where useful:
<x-blocks.product-category-boutique />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
'eyebrow' => 'The Collection',
'title' => 'All Bags',
'subheading' => 'A considered edit of structured leather pieces — filtered to find the one that carries from one season into the next.',
'count' => null,
'categories' => null,
'materials' => null,
'colors' => null,
'price' => null,
'products' => null,
'currency' => 'EUR',
'locale' => 'en-IE',
])
{{--
product-category-04 — "Boutique" Monogram editorial faceted collection page.
A family member of product-category-filtered (the same faceted filter + grid layout)
re-cut in the editorial Monogram register: a slim header (font-heading collection
title, eyebrow, result count and a sort select), a left filter rail (category
list, min/max price range, material checkboxes and token-only colour swatches)
and a responsive grid of product cards (name, material line, price with an
optional compare-at and a sale badge; no rating, wishlist or quick-add, so the
grid keeps its calm editorial register). On small screens the rail collapses
behind an inline-Alpine "Filters" drawer; sort is the select primitive.
Composes <x-ui.product-card>, <x-ui.select>, <x-ui.color-swatch>, <x-ui.button>,
<x-ui.checkbox>, <x-ui.input>, <x-ui.separator>.
Semantic tokens only, logical spacing, WCAG AA, RTL-safe, reduced-motion.
--}}
@php
$categories ??= [
['label' => 'Totes', 'count' => 48, 'checked' => true],
['label' => 'Crossbody', 'count' => 36, 'checked' => false],
['label' => 'Top Handle', 'count' => 27, 'checked' => false],
['label' => 'Clutches', 'count' => 19, 'checked' => false],
['label' => 'Backpacks', 'count' => 22, 'checked' => false],
['label' => 'Travel', 'count' => 14, 'checked' => false],
];
$materials ??= [
['label' => 'Calf leather', 'checked' => false],
['label' => 'Pebbled grain', 'checked' => true],
['label' => 'Suede', 'checked' => false],
['label' => 'Recycled canvas','checked' => false],
];
// Colour facets use semantic token classes — never raw colours.
$colors ??= [
['label' => 'Onyx', 'class' => 'bg-foreground', 'active' => true],
['label' => 'Stone', 'class' => 'bg-muted-foreground', 'active' => false],
['label' => 'Sand', 'class' => 'bg-secondary', 'active' => false],
['label' => 'Cognac', 'class' => 'bg-accent', 'active' => false],
['label' => 'Ember', 'class' => 'bg-primary', 'active' => false],
['label' => 'Crimson', 'class' => 'bg-destructive', 'active' => false],
];
$price ??= ['min' => 120, 'max' => 980];
$products ??= [
['name' => 'Aurelia Structured Tote', 'meta' => 'Calf leather, Onyx', 'price' => '680', 'was' => null, 'badge' => null, 'badgeVariant' => 'secondary'],
['name' => 'Margaux Crossbody', 'meta' => 'Pebbled grain, Sand', 'price' => '420', 'was' => '520', 'badge' => 'Sale', 'badgeVariant' => 'destructive'],
['name' => 'Colette Top Handle', 'meta' => 'Calf leather, Cognac', 'price' => '740', 'was' => null, 'badge' => 'New', 'badgeVariant' => 'default'],
['name' => 'Esme Evening Clutch', 'meta' => 'Satin, Onyx', 'price' => '310', 'was' => null, 'badge' => null, 'badgeVariant' => 'secondary'],
['name' => 'Juno Backpack', 'meta' => 'Recycled canvas, Stone', 'price' => '360', 'was' => '440', 'badge' => 'Sale', 'badgeVariant' => 'destructive'],
['name' => 'Liv Weekender', 'meta' => 'Pebbled grain, Ember', 'price' => '980', 'was' => null, 'badge' => null, 'badgeVariant' => 'secondary'],
];
$activeColor = collect($colors)->first(fn ($color) => ! empty($color['active']))['label'] ?? null;
$count ??= count($products);
@endphp
<section
{{ $attributes->merge(['class' => 'bg-background py-14 text-foreground sm:py-20']) }}
data-slot="product-category"
aria-label="{{ __($title) }}"
x-data="{ filtersOpen: false, sort: 'featured' }"
>
<div class="mx-auto max-w-7xl px-6 sm:px-8 lg:px-12">
{{-- ── Slim collection header ── --}}
<header class="flex flex-col gap-6 border-b border-border pb-8 sm:flex-row sm:items-end sm:justify-between">
<div class="max-w-2xl">
@if ($eyebrow)
<p class="font-body text-sm uppercase tracking-[0.3em] text-muted-foreground">
{{ __($eyebrow) }}
</p>
@endif
<h1 class="mt-4 font-heading text-3xl font-normal leading-[1.1] tracking-tight text-balance sm:text-4xl lg:text-5xl">
{{ __($title) }}
</h1>
@if ($subheading)
<p class="mt-4 max-w-md font-body text-sm leading-relaxed text-muted-foreground text-pretty">
{{ __($subheading) }}
</p>
@endif
</div>
<div class="flex items-center justify-between gap-4 sm:flex-col sm:items-end">
<p class="font-body text-sm text-muted-foreground">
{{ trans_choice('{0}No pieces|{1}:count piece|[2,*]:count pieces', $count, ['count' => $count]) }}
</p>
<label class="flex items-center gap-2 font-body text-sm text-muted-foreground">
<span class="shrink-0 uppercase tracking-wide text-sm">{{ __('Sort') }}</span>
<x-ui.select size="sm" name="sort" x-model="sort" class="w-48">
<option value="featured">{{ __('Featured') }}</option>
<option value="newest">{{ __('Newest') }}</option>
<option value="price-asc">{{ __('Price: low to high') }}</option>
<option value="price-desc">{{ __('Price: high to low') }}</option>
</x-ui.select>
</label>
</div>
</header>
{{-- ── Mobile filter trigger ── --}}
<div class="mt-6 lg:hidden">
<x-ui.button
type="button"
variant="outline"
class="w-full"
x-on:click="filtersOpen = true"
x-bind:aria-expanded="filtersOpen.toString()"
aria-controls="product-category-04-filters"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 6h16M7 12h10M10 18h4" />
</svg>
{{ __('Filters') }}
</x-ui.button>
</div>
<div class="mt-8 flex flex-col gap-10 lg:flex-row">
{{-- ── Filter rail (drawer < lg, static rail ≥ lg) ── --}}
<div
class="fixed inset-0 z-40 bg-scrim/40 backdrop-blur-sm transition-opacity lg:hidden"
x-show="filtersOpen"
x-transition.opacity
x-on:click="filtersOpen = false"
x-cloak
aria-hidden="true"
></div>
<aside
id="product-category-04-filters"
class="fixed inset-y-0 start-0 z-50 w-80 max-w-[85vw] overflow-y-auto border-e border-border bg-background p-6 shadow-xl transition-transform duration-300 ease-out lg:static lg:z-auto lg:w-60 lg:max-w-none lg:shrink-0 lg:overflow-visible lg:border-e-0 lg:bg-transparent lg:p-0 lg:shadow-none rtl:shadow-xl"
x-bind:class="filtersOpen ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full lg:translate-x-0 rtl:lg:translate-x-0'"
aria-label="{{ __('Product filters') }}"
>
<div class="mb-6 flex items-center justify-between lg:hidden">
<span class="font-heading text-lg font-normal tracking-tight">{{ __('Filters') }}</span>
<x-ui.button
type="button"
variant="ghost"
size="icon"
x-on:click="filtersOpen = false"
aria-label="{{ __('Close filters') }}"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</x-ui.button>
</div>
{{-- Category --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Category') }}</legend>
<div class="flex flex-col gap-4">
@foreach ($categories as $category)
<label class="flex cursor-pointer items-center gap-2 font-body text-sm">
<x-ui.checkbox :checked="$category['checked'] ?? false" name="category[]" :value="$category['label']" />
<span class="flex-1 text-foreground">{{ __($category['label']) }}</span>
<span class="text-sm tabular-nums text-muted-foreground">{{ $category['count'] }}</span>
</label>
@endforeach
</div>
</fieldset>
<x-ui.separator class="my-6" />
{{-- Price range --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Price') }}</legend>
<div class="flex items-end gap-4">
<label class="flex-1">
<span class="mb-2 block font-body text-sm text-muted-foreground">{{ __('Min') }}</span>
<x-ui.input type="number" inputmode="numeric" min="0" name="price_min" :value="$price['min']" aria-label="{{ __('Minimum price') }}" />
</label>
<span class="pb-2 text-muted-foreground" aria-hidden="true">–</span>
<label class="flex-1">
<span class="mb-2 block font-body text-sm text-muted-foreground">{{ __('Max') }}</span>
<x-ui.input type="number" inputmode="numeric" min="0" name="price_max" :value="$price['max']" aria-label="{{ __('Maximum price') }}" />
</label>
</div>
</fieldset>
<x-ui.separator class="my-6" />
{{-- Material --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Material') }}</legend>
<div class="flex flex-col gap-4">
@foreach ($materials as $material)
<label class="flex cursor-pointer items-center gap-2 font-body text-sm">
<x-ui.checkbox :checked="$material['checked'] ?? false" name="material[]" :value="$material['label']" />
<span class="flex-1 text-foreground">{{ __($material['label']) }}</span>
</label>
@endforeach
</div>
</fieldset>
<x-ui.separator class="my-6" />
{{-- Colour --}}
<fieldset>
<legend class="mb-4 font-body text-sm font-semibold uppercase tracking-[0.2em] text-muted-foreground">{{ __('Colour') }}</legend>
<x-ui.color-swatch name="colour" size="sm" :label="__('Colour')" :colors="$colors" :selected="$activeColor" />
</fieldset>
<x-ui.separator class="my-6" />
<x-ui.button variant="outline" size="sm" class="w-full">{{ __('Clear all') }}</x-ui.button>
</aside>
{{-- ── Product grid ── --}}
<div class="min-w-0 flex-1">
<ul role="list" class="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($products as $product)
@php
// `meta` / `was` are this block's data keys; the card reads
// `description` / `compareAt`, so map them here.
$card = [
'name' => $product['name'],
'href' => $product['href'] ?? '#',
'description' => $product['meta'] ?? $product['description'] ?? null,
'price' => $product['price'],
'compareAt' => $product['was'] ?? $product['compareAt'] ?? null,
'badge' => $product['badge'] ?? null,
'badgeVariant' => $product['badgeVariant'] ?? 'default',
'gradient' => 'from-muted to-muted',
];
@endphp
<li>
<x-ui.product-card :product="$card" :currency="$currency" :locale="$locale" heading-level="h2" :show-rating="false" :show-wishlist="false" :show-actions="false" />
</li>
@endforeach
</ul>
{{-- Load more --}}
<div class="mt-14 flex justify-center">
<x-ui.button variant="outline">{{ __('Load more') }}</x-ui.button>
</div>
</div>
</div>
</div>
</section>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.2 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v1 - Foundation
-
≥ 1.0.0