Skip to content
Brok UI

Loading…

No results

Product Category

Open source

Category pages — a banner with a faceted filter sidebar (categories, price, swatches) and a product grid.

Preview

resources/views/blocks/product-category-boutique.blade.php Blade
@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

terminal
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

product-card select color-swatch button checkbox input separator

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.

product-category-boutique.md
# 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
collection header eyebrow title subheading result count sort select mobile filter trigger filter rail category checkboxes price range material checkboxes colour swatches product grid

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.product-category-boutique />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

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.

resources/views/blocks/product-category-boutique.blade.php Blade
@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