Skip to content
Brok UI

Loading…

No results

Product Overview

Open source

Product detail (PDP) layouts — an image gallery beside title, rating, price, colour/size selectors and add-to-cart.

Preview

resources/views/blocks/product-overview-editorial.blade.php Blade
@props([
    'brand'        => 'Monogram',
    'name'         => 'Curved Shoulder Bag',
    'price'        => '$248.00',
    'comparePrice' => null,
    'rating'       => 4.8,
    'reviewCount'  => 64,
    'description'  => 'A softly structured shoulder bag in full-grain leather, hand-finished with an arced top line and a single considered strap — an everyday piece made to outlast the season.',
    'colors'       => null,
    'sizes'        => null,
    'images'       => null,
    'sections'     => null,
    'action'       => '#',
])

{{--
    product-overview-04 — "Editorial" Monogram product detail page (PDP).
    A distinct sibling in the product-overview family (vs product-overview-01's
    card-led two-column PDP): an editorial, whitespace-led layout — a large
    `bg-muted aspect-square` media placeholder with a thumbnail strip that swaps
    the main view via inline Alpine, beside a details column with an uppercase
    wide-tracked brand eyebrow, a large `font-heading` title, price + rating,
    colour swatches, size chips, a quantity stepper,
    add-to-cart / wishlist actions, and collapsible Details / Shipping rows.
    No external images or JS module — inline Alpine only.
    Composes <x-ui.badge>, <x-ui.rating>, <x-ui.color-swatch>, <x-ui.option-group>,
    <x-ui.number-field>, <x-ui.button>, <x-ui.separator>.
    Semantic tokens only, logical spacing, WCAG AA, RTL-safe.
--}}
@php
    // Swatches reference semantic tokens by class name (never raw hex), so a
    // re-theme recolours the dots automatically and the raw-colour guard passes.
    $colors ??= [
        ['name' => 'Charcoal', 'class' => 'bg-foreground'],
        ['name' => 'Taupe',    'class' => 'bg-muted-foreground'],
        ['name' => 'Sand',     'class' => 'bg-secondary'],
        ['name' => 'Clay',     'class' => 'bg-accent'],
    ];

    $sizes ??= ['Small', 'Medium', 'Large'];

    // Gallery thumbnails: each is a label for a decorative tinted panel, so the
    // block renders standalone without bundling binary image assets.
    $images ??= ['Front', 'Angle', 'Interior', 'Detail'];

    // Collapsible editorial rows.
    $sections ??= [
        [
            'key'   => 'details',
            'title' => 'Details',
            'lines' => [
                'Full-grain Italian leather with a natural pull-up finish.',
                'Cotton-twill lining with a slip pocket and a zip pocket.',
                'Solid brass hardware; adjustable shoulder strap.',
            ],
        ],
        [
            'key'   => 'shipping',
            'title' => 'Shipping & Returns',
            'lines' => [
                'Complimentary carbon-neutral shipping on orders over $75.',
                'Free 30-day returns — unworn, with tags attached.',
                'Each piece ships in recycled, plastic-free packaging.',
            ],
        ],
    ];

    $first = $sections[0]['key'] ?? null;

    // <x-ui.color-swatch> takes value/label/token entries; the Alpine `color`
    // state holds the option value and the legend reads its label from a map.
    $swatches = collect($colors)->map(fn ($color) => [
        'value' => $color['value'] ?? $color['name'],
        'label' => __($color['name']),
        'token' => $color['class'],
    ])->values()->all();
    $colorLabels = collect($swatches)->pluck('label', 'value')->all();
@endphp

<section
    x-data="{
        color: @js($swatches[0]['value'] ?? null),
        size: null,
        qty: 1,
        active: 0,
        panel: @js($first),
        images: [@foreach ($images as $image)@js(__($image)){{ $loop->last ? '' : ', ' }}@endforeach],
        colors: @js($colorLabels),
        toggle(key) { this.panel = this.panel === key ? null : key },
    }"
    {{ $attributes->merge(['class' => 'bg-background py-12 text-foreground sm:py-16 lg:py-20']) }}
    data-slot="product-overview"
    aria-label="{{ __($name) }}"
>
    <div class="mx-auto grid max-w-6xl grid-cols-1 gap-10 px-6 sm:px-8 lg:grid-cols-2 lg:gap-16 lg:px-12">
        {{-- ── Gallery ── --}}
        <div class="flex flex-col-reverse gap-4 lg:flex-row">
            {{-- Thumbnail strip --}}
            <ul class="grid grid-cols-4 gap-4 lg:w-20 lg:shrink-0 lg:grid-cols-1" role="list">
                @foreach ($images as $i => $image)
                    <li>
                        <button
                            type="button"
                            @click="active = {{ $i }}"
                            :class="active === {{ $i }} ? 'border-foreground ring-1 ring-foreground' : 'border-border hover:border-foreground/40'"
                            :aria-pressed="active === {{ $i }} ? 'true' : 'false'"
                            class="flex aspect-square w-full items-center justify-center border bg-muted text-muted-foreground transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                            aria-label="{{ __('Show :view view', ['view' => __($image)]) }}"
                        >
                            <svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <path d="M6 21V9a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12" />
                                <path d="M9 6V5a3 3 0 0 1 6 0v1" />
                                <path d="M4 21h16" />
                            </svg>
                        </button>
                    </li>
                @endforeach
            </ul>

            {{-- Main media placeholder --}}
            <div
                class="relative flex aspect-square flex-1 items-center justify-center overflow-hidden bg-muted"
                role="img"
                :aria-label="@js(__($name)) + ' — ' + images[active]"
            >
                <svg class="size-32 text-muted-foreground/40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <path d="M6 21V9a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12" />
                    <path d="M9 6V5a3 3 0 0 1 6 0v1" />
                    <path d="M4 21h16" />
                </svg>
                <span class="absolute bottom-4 end-4 font-body text-sm uppercase tracking-widest text-muted-foreground" x-text="images[active]"></span>
            </div>
        </div>

        {{-- ── Details ── --}}
        <div class="flex flex-col gap-8">
            <div class="flex flex-col gap-4">
                @if ($brand)
                    <div>
                        <x-ui.badge variant="secondary" class="font-body text-sm uppercase tracking-[0.3em]">{{ __($brand) }}</x-ui.badge>
                    </div>
                @endif

                <h1 class="font-heading text-balance text-4xl font-normal leading-[1.05] tracking-tight sm:text-5xl">{{ __($name) }}</h1>

                <div class="flex flex-wrap items-center gap-4">
                    <x-ui.rating :value="$rating" readOnly size="sm" />
                    <span class="text-sm font-medium text-foreground">{{ number_format((float) $rating, 1) }}</span>
                    <span class="text-sm text-muted-foreground">{{ trans_choice('(:count review)|(:count reviews)', (int) $reviewCount, ['count' => number_format((int) $reviewCount)]) }}</span>
                </div>

                <div class="flex items-baseline gap-4">
                    <span class="text-2xl font-semibold tracking-tight">{{ $price }}</span>
                    @if ($comparePrice)
                        <span class="text-sm text-muted-foreground line-through">{{ $comparePrice }}</span>
                    @endif
                </div>
            </div>

            <p class="max-w-md text-pretty font-body text-sm leading-relaxed text-muted-foreground">{{ __($description) }}</p>

            {{-- Colour swatches --}}
            <fieldset class="flex flex-col gap-4">
                <legend class="font-body text-sm font-medium uppercase tracking-[0.2em] text-foreground">
                    {{ __('Colour') }}
                    <span class="ms-2 normal-case tracking-normal text-muted-foreground" x-text="colors[color]"></span>
                </legend>
                <x-ui.color-swatch name="color" model="color" :selected="$swatches[0]['value'] ?? null" :colors="$swatches" />
            </fieldset>

            {{-- Size tiles --}}
            <fieldset class="flex flex-col gap-4">
                <div class="flex items-center justify-between">
                    <legend class="font-body text-sm font-medium uppercase tracking-[0.2em] text-foreground">{{ __('Size') }}</legend>
                    <a href="#size-guide" class="text-sm font-medium text-foreground underline-offset-4 hover:underline">{{ __('Size guide') }}</a>
                </div>
                <x-ui.option-group name="size" model="size" :options="$sizes" />
            </fieldset>

            {{-- Quantity + actions --}}
            <form method="POST" action="{{ $action }}" class="flex flex-col gap-4">
                @csrf
                <input type="hidden" name="color" x-bind:value="color" />
                <input type="hidden" name="size" x-bind:value="size" />
                <input type="hidden" name="quantity" x-bind:value="qty" />

                <div class="flex flex-col gap-4 sm:flex-row sm:items-stretch">
                    <x-ui.number-field variant="stepper" size="lg" x-model="qty" :min="1" :max="10" :label="__('Quantity')" class="self-start sm:self-auto" />

                    <x-ui.button type="submit" size="lg" class="h-12 flex-1">
                        <x-slot:icon>
                            <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <circle cx="9" cy="21" r="1" />
                                <circle cx="20" cy="21" r="1" />
                                <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
                            </svg>
                        </x-slot:icon>
                        {{ __('Add to cart') }}
                    </x-ui.button>

                    <x-ui.button type="button" variant="outline" size="lg" class="h-12" aria-label="{{ __('Add to wishlist') }}">
                        <x-slot:icon>
                            <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78Z" />
                            </svg>
                        </x-slot:icon>
                        <span class="sr-only sm:not-sr-only">{{ __('Wishlist') }}</span>
                    </x-ui.button>
                </div>
            </form>

            <x-ui.separator />

            {{-- Collapsible editorial rows --}}
            <div class="flex flex-col">
                @foreach ($sections as $section)
                    @php $key = $section['key']; $panelId = 'product-overview-04-'.$key; @endphp
                    <div class="border-b border-border">
                        <h2>
                            <button
                                type="button"
                                @click="toggle(@js($key))"
                                :aria-expanded="panel === @js($key) ? 'true' : 'false'"
                                aria-controls="{{ $panelId }}"
                                class="flex w-full items-center justify-between gap-4 py-4 text-start font-body text-sm font-medium uppercase tracking-[0.2em] text-foreground transition-colors hover:text-foreground/70 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                            >
                                {{ __($section['title']) }}
                                <svg class="size-4 shrink-0 text-muted-foreground transition-transform duration-200" :class="panel === @js($key) ? 'rotate-180' : ''" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                    <path d="m6 9 6 6 6-6" />
                                </svg>
                            </button>
                        </h2>
                        <div
                            id="{{ $panelId }}"
                            x-show="panel === @js($key)"
                            x-transition.opacity.duration.200ms
                            class="pb-6"
                        >
                            <ul class="flex flex-col gap-2 font-body text-sm leading-relaxed text-muted-foreground" role="list">
                                @foreach ($section['lines'] as $line)
                                    <li>{{ __($line) }}</li>
                                @endforeach
                            </ul>
                        </div>
                    </div>
                @endforeach
            </div>
        </div>
    </div>
</section>

Installation

terminal
php artisan ui:add blocks/product-overview-editorial

Registry contract

Install confidence

php artisan ui:add blocks/product-overview-editorial 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.1
License
open
Stability
stable
Contract
v1
Foundation
≥ 1.0.0
Type Generated target
blade resources/views/blocks/product-overview-editorial.blade.php

Registry dependencies

badge button rating separator color-swatch option-group number-field

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-overview-editorial.md
# Brok UI block: Product Overview Editorial (`product-overview-editorial`)

An editorial Monogram product detail page with a large image gallery and thumbnail strip, brand eyebrow, font-heading title, price, rating, colour swatches, size tiles, a quantity stepper, add-to-cart and wishlist buttons, and collapsible Details and Shipping & Returns rows.

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-overview-editorial
```

## Render it

```blade
<x-blocks.product-overview-editorial />
```

## Props

- `brand` (string, default `Monogram`) — Brand name shown in a badge above the title.
- `name` (string, default `Curved Shoulder Bag`) — Product name / page title.
- `price` (string, default `$248.00`) — Current price, pre-formatted.
- `compare-price` (mixed|null, default `null`) — Struck-through original price; null hides it.
- `rating` (float, default `4.8`) — Average rating value rendered by the read-only rating stars.
- `review-count` (int, default `64`) — Number of reviews shown beside the rating.
- `description` (string, default `A softly structured shoulder bag in full-grain leather, hand-finished with an arced top line and a single considered strap — an everyday piece made to outlast the season.`) — Product description copy.
- `colors` (mixed|null, default `null`) — Array of {name, class} colour swatch entries using semantic token classes.
- `sizes` (mixed|null, default `null`) — Array of size option label strings.
- `images` (mixed|null, default `null`) — Array of gallery image labels used for the thumbnail strip and main image caption.
- `sections` (mixed|null, default `null`) — Array of {key, title, lines} collapsible section entries (e.g. Details, Shipping & returns).
- `action` (string, default `#`) — Form action URL submitted by the add-to-cart form.

## Use when

- Building an editorial Monogram-register product detail page with a large switchable image gallery and thumbnail strip, brand eyebrow, font-heading title, price, rating, colour swatches, size tiles, a quantity stepper and add-to-cart/wishlist buttons.
- Needing collapsible "Details" and "Shipping & returns" disclosure rows below the buying options instead of a static feature list.

## Avoid when

- Needing the standard (non-editorial) commerce register with a static feature list instead of collapsible sections; use product-overview-detail instead.

## Rules

- Render the installed block with `<x-blocks.product-overview-editorial />` 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-overview-editorial
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/product-overview-editorial.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 an editorial Monogram-register product detail page with a large switchable image gallery and thumbnail strip, brand eyebrow, font-heading title, price, rating, colour swatches, size tiles, a quantity stepper and add-to-cart/wishlist buttons.
  • Needing collapsible "Details" and "Shipping & returns" disclosure rows below the buying options instead of a static feature list.

Avoid when

  • Needing the standard (non-editorial) commerce register with a static feature list instead of collapsible sections; use product-overview-detail instead.

Anti-patterns

  • Do not replace semantic props with conflicting utility classes.
  • Do not remove labels, focus styles, or state attributes.
Anatomy
gallery thumbnail strip main image product details brand badge title rating price colour swatches size selector quantity stepper add to cart form collapsible sections

Usage

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

resources/views/welcome.blade.php Blade
<x-blocks.product-overview-editorial />

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-overview-editorial.blade.php Blade
@props([
    'brand'        => 'Monogram',
    'name'         => 'Curved Shoulder Bag',
    'price'        => '$248.00',
    'comparePrice' => null,
    'rating'       => 4.8,
    'reviewCount'  => 64,
    'description'  => 'A softly structured shoulder bag in full-grain leather, hand-finished with an arced top line and a single considered strap — an everyday piece made to outlast the season.',
    'colors'       => null,
    'sizes'        => null,
    'images'       => null,
    'sections'     => null,
    'action'       => '#',
])

{{--
    product-overview-04 — "Editorial" Monogram product detail page (PDP).
    A distinct sibling in the product-overview family (vs product-overview-01's
    card-led two-column PDP): an editorial, whitespace-led layout — a large
    `bg-muted aspect-square` media placeholder with a thumbnail strip that swaps
    the main view via inline Alpine, beside a details column with an uppercase
    wide-tracked brand eyebrow, a large `font-heading` title, price + rating,
    colour swatches, size chips, a quantity stepper,
    add-to-cart / wishlist actions, and collapsible Details / Shipping rows.
    No external images or JS module — inline Alpine only.
    Composes <x-ui.badge>, <x-ui.rating>, <x-ui.color-swatch>, <x-ui.option-group>,
    <x-ui.number-field>, <x-ui.button>, <x-ui.separator>.
    Semantic tokens only, logical spacing, WCAG AA, RTL-safe.
--}}
@php
    // Swatches reference semantic tokens by class name (never raw hex), so a
    // re-theme recolours the dots automatically and the raw-colour guard passes.
    $colors ??= [
        ['name' => 'Charcoal', 'class' => 'bg-foreground'],
        ['name' => 'Taupe',    'class' => 'bg-muted-foreground'],
        ['name' => 'Sand',     'class' => 'bg-secondary'],
        ['name' => 'Clay',     'class' => 'bg-accent'],
    ];

    $sizes ??= ['Small', 'Medium', 'Large'];

    // Gallery thumbnails: each is a label for a decorative tinted panel, so the
    // block renders standalone without bundling binary image assets.
    $images ??= ['Front', 'Angle', 'Interior', 'Detail'];

    // Collapsible editorial rows.
    $sections ??= [
        [
            'key'   => 'details',
            'title' => 'Details',
            'lines' => [
                'Full-grain Italian leather with a natural pull-up finish.',
                'Cotton-twill lining with a slip pocket and a zip pocket.',
                'Solid brass hardware; adjustable shoulder strap.',
            ],
        ],
        [
            'key'   => 'shipping',
            'title' => 'Shipping & Returns',
            'lines' => [
                'Complimentary carbon-neutral shipping on orders over $75.',
                'Free 30-day returns — unworn, with tags attached.',
                'Each piece ships in recycled, plastic-free packaging.',
            ],
        ],
    ];

    $first = $sections[0]['key'] ?? null;

    // <x-ui.color-swatch> takes value/label/token entries; the Alpine `color`
    // state holds the option value and the legend reads its label from a map.
    $swatches = collect($colors)->map(fn ($color) => [
        'value' => $color['value'] ?? $color['name'],
        'label' => __($color['name']),
        'token' => $color['class'],
    ])->values()->all();
    $colorLabels = collect($swatches)->pluck('label', 'value')->all();
@endphp

<section
    x-data="{
        color: @js($swatches[0]['value'] ?? null),
        size: null,
        qty: 1,
        active: 0,
        panel: @js($first),
        images: [@foreach ($images as $image)@js(__($image)){{ $loop->last ? '' : ', ' }}@endforeach],
        colors: @js($colorLabels),
        toggle(key) { this.panel = this.panel === key ? null : key },
    }"
    {{ $attributes->merge(['class' => 'bg-background py-12 text-foreground sm:py-16 lg:py-20']) }}
    data-slot="product-overview"
    aria-label="{{ __($name) }}"
>
    <div class="mx-auto grid max-w-6xl grid-cols-1 gap-10 px-6 sm:px-8 lg:grid-cols-2 lg:gap-16 lg:px-12">
        {{-- ── Gallery ── --}}
        <div class="flex flex-col-reverse gap-4 lg:flex-row">
            {{-- Thumbnail strip --}}
            <ul class="grid grid-cols-4 gap-4 lg:w-20 lg:shrink-0 lg:grid-cols-1" role="list">
                @foreach ($images as $i => $image)
                    <li>
                        <button
                            type="button"
                            @click="active = {{ $i }}"
                            :class="active === {{ $i }} ? 'border-foreground ring-1 ring-foreground' : 'border-border hover:border-foreground/40'"
                            :aria-pressed="active === {{ $i }} ? 'true' : 'false'"
                            class="flex aspect-square w-full items-center justify-center border bg-muted text-muted-foreground transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                            aria-label="{{ __('Show :view view', ['view' => __($image)]) }}"
                        >
                            <svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <path d="M6 21V9a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12" />
                                <path d="M9 6V5a3 3 0 0 1 6 0v1" />
                                <path d="M4 21h16" />
                            </svg>
                        </button>
                    </li>
                @endforeach
            </ul>

            {{-- Main media placeholder --}}
            <div
                class="relative flex aspect-square flex-1 items-center justify-center overflow-hidden bg-muted"
                role="img"
                :aria-label="@js(__($name)) + ' — ' + images[active]"
            >
                <svg class="size-32 text-muted-foreground/40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                    <path d="M6 21V9a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12" />
                    <path d="M9 6V5a3 3 0 0 1 6 0v1" />
                    <path d="M4 21h16" />
                </svg>
                <span class="absolute bottom-4 end-4 font-body text-sm uppercase tracking-widest text-muted-foreground" x-text="images[active]"></span>
            </div>
        </div>

        {{-- ── Details ── --}}
        <div class="flex flex-col gap-8">
            <div class="flex flex-col gap-4">
                @if ($brand)
                    <div>
                        <x-ui.badge variant="secondary" class="font-body text-sm uppercase tracking-[0.3em]">{{ __($brand) }}</x-ui.badge>
                    </div>
                @endif

                <h1 class="font-heading text-balance text-4xl font-normal leading-[1.05] tracking-tight sm:text-5xl">{{ __($name) }}</h1>

                <div class="flex flex-wrap items-center gap-4">
                    <x-ui.rating :value="$rating" readOnly size="sm" />
                    <span class="text-sm font-medium text-foreground">{{ number_format((float) $rating, 1) }}</span>
                    <span class="text-sm text-muted-foreground">{{ trans_choice('(:count review)|(:count reviews)', (int) $reviewCount, ['count' => number_format((int) $reviewCount)]) }}</span>
                </div>

                <div class="flex items-baseline gap-4">
                    <span class="text-2xl font-semibold tracking-tight">{{ $price }}</span>
                    @if ($comparePrice)
                        <span class="text-sm text-muted-foreground line-through">{{ $comparePrice }}</span>
                    @endif
                </div>
            </div>

            <p class="max-w-md text-pretty font-body text-sm leading-relaxed text-muted-foreground">{{ __($description) }}</p>

            {{-- Colour swatches --}}
            <fieldset class="flex flex-col gap-4">
                <legend class="font-body text-sm font-medium uppercase tracking-[0.2em] text-foreground">
                    {{ __('Colour') }}
                    <span class="ms-2 normal-case tracking-normal text-muted-foreground" x-text="colors[color]"></span>
                </legend>
                <x-ui.color-swatch name="color" model="color" :selected="$swatches[0]['value'] ?? null" :colors="$swatches" />
            </fieldset>

            {{-- Size tiles --}}
            <fieldset class="flex flex-col gap-4">
                <div class="flex items-center justify-between">
                    <legend class="font-body text-sm font-medium uppercase tracking-[0.2em] text-foreground">{{ __('Size') }}</legend>
                    <a href="#size-guide" class="text-sm font-medium text-foreground underline-offset-4 hover:underline">{{ __('Size guide') }}</a>
                </div>
                <x-ui.option-group name="size" model="size" :options="$sizes" />
            </fieldset>

            {{-- Quantity + actions --}}
            <form method="POST" action="{{ $action }}" class="flex flex-col gap-4">
                @csrf
                <input type="hidden" name="color" x-bind:value="color" />
                <input type="hidden" name="size" x-bind:value="size" />
                <input type="hidden" name="quantity" x-bind:value="qty" />

                <div class="flex flex-col gap-4 sm:flex-row sm:items-stretch">
                    <x-ui.number-field variant="stepper" size="lg" x-model="qty" :min="1" :max="10" :label="__('Quantity')" class="self-start sm:self-auto" />

                    <x-ui.button type="submit" size="lg" class="h-12 flex-1">
                        <x-slot:icon>
                            <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <circle cx="9" cy="21" r="1" />
                                <circle cx="20" cy="21" r="1" />
                                <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" />
                            </svg>
                        </x-slot:icon>
                        {{ __('Add to cart') }}
                    </x-ui.button>

                    <x-ui.button type="button" variant="outline" size="lg" class="h-12" aria-label="{{ __('Add to wishlist') }}">
                        <x-slot:icon>
                            <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78Z" />
                            </svg>
                        </x-slot:icon>
                        <span class="sr-only sm:not-sr-only">{{ __('Wishlist') }}</span>
                    </x-ui.button>
                </div>
            </form>

            <x-ui.separator />

            {{-- Collapsible editorial rows --}}
            <div class="flex flex-col">
                @foreach ($sections as $section)
                    @php $key = $section['key']; $panelId = 'product-overview-04-'.$key; @endphp
                    <div class="border-b border-border">
                        <h2>
                            <button
                                type="button"
                                @click="toggle(@js($key))"
                                :aria-expanded="panel === @js($key) ? 'true' : 'false'"
                                aria-controls="{{ $panelId }}"
                                class="flex w-full items-center justify-between gap-4 py-4 text-start font-body text-sm font-medium uppercase tracking-[0.2em] text-foreground transition-colors hover:text-foreground/70 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                            >
                                {{ __($section['title']) }}
                                <svg class="size-4 shrink-0 text-muted-foreground transition-transform duration-200" :class="panel === @js($key) ? 'rotate-180' : ''" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                    <path d="m6 9 6 6 6-6" />
                                </svg>
                            </button>
                        </h2>
                        <div
                            id="{{ $panelId }}"
                            x-show="panel === @js($key)"
                            x-transition.opacity.duration.200ms
                            class="pb-6"
                        >
                            <ul class="flex flex-col gap-2 font-body text-sm leading-relaxed text-muted-foreground" role="list">
                                @foreach ($section['lines'] as $line)
                                    <li>{{ __($line) }}</li>
                                @endforeach
                            </ul>
                        </div>
                    </div>
                @endforeach
            </div>
        </div>
    </div>
</section>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.1.1
Status
Stable
License
open
Deprecation
Not deprecated
Contract
v1
Foundation
≥ 1.0.0