category page filters components and general ui - no functionality yet

This commit is contained in:
elvira
2026-08-31 21:52:47 +03:00
parent 74e554884f
commit b070a7d1e6
13 changed files with 1491 additions and 1620 deletions
Generated
+985 -1550
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,5 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "./fonts.css"; @import "./fonts.css";
@import "./dropdown.css";
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php'; @source '../../storage/framework/views/*.php';
@source '../**/*.blade.php'; @source '../**/*.blade.php';
@@ -20,7 +21,7 @@ @theme {
--text-h1: 60px; --text-h1: 60px;
--text-h2: 48px; --text-h2: 48px;
--text-h3: 36px; --text-h3: 36px;
--text-h4: 27px; --text-h4: 26px;
} }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
+40
View File
@@ -0,0 +1,40 @@
/* ── Dropdown (Popover API) ──────────────────────────────────── */
/* The panel is a [popover] → it renders in the top layer, so its
containing block is the viewport, not the .dropdown wrapper, and
CSS alone can't tie it to the trigger (anchor positioning isn't
everywhere yet). The `dropdown` Stimulus controller measures the
trigger on open and writes --dropdown-top/left/width here; the
open/close animation below stays pure CSS. Opens on a click, so
animating transform + opacity is CLS-safe. */
.dropdown-panel {
top: var(--dropdown-top, 0);
left: var(--dropdown-left, 0);
min-width: var(--dropdown-width, 0);
opacity: 0;
transform: translateY(-4px);
transition:
opacity 0.2s ease,
transform 0.2s ease,
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
.dropdown-panel:popover-open {
opacity: 1;
transform: translateY(-1px);
}
@starting-style {
.dropdown-panel:popover-open {
opacity: 0;
transform: translateY(-4px);
}
}
/* Caret flips while the panel is open — :has() is the only route
back up from the popover's :popover-open state to the caret. */
.dropdown:has(.dropdown-panel:popover-open) .dropdown-caret {
transform: rotate(180deg);
}
@@ -1,17 +0,0 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['menu']
open() {
this.menuTarget.classList.add('is-open')
}
close() {
this.menuTarget.classList.remove('is-open')
}
toggle() {
this.menuTarget.classList.toggle('is-open')
}
}
@@ -0,0 +1,27 @@
import { Controller } from '@hotwired/stimulus'
// Positions the popover panel directly under its trigger.
//
// A [popover] renders in the top layer, so its containing block is the
// viewport, not the .dropdown wrapper — CSS alone can't tie it to the trigger
// without anchor positioning, which isn't in every browser yet. So on each
// open we measure the trigger and write the geometry to CSS custom properties
// that .dropdown-panel consumes (top / left / min-width). The open/close
// animation stays entirely in CSS; the controller only feeds it three numbers.
export default class extends Controller {
static targets = ['trigger', 'panel']
// Wired to `click->dropdown#position` on the trigger, which also fires for
// keyboard activation (Enter/Space on a <button>), so this runs before the
// native popover toggle paints the panel.
position() {
const rect = this.triggerTarget.getBoundingClientRect()
const style = this.panelTarget.style
style.setProperty('--dropdown-top', `${rect.bottom + window.scrollY}px`)
style.setProperty('--dropdown-left', `${rect.left + window.scrollX}px`)
style.setProperty('--dropdown-width', `${rect.width}px`)
}
}
+4
View File
@@ -6,9 +6,11 @@
import AppearController from './appear-controller' import AppearController from './appear-controller'
import BackToTopController from './back-to-top-controller' import BackToTopController from './back-to-top-controller'
import CarouselController from './carousel-controller' import CarouselController from './carousel-controller'
import DropdownController from './dropdown-controller'
import ProductFormController from './product-form-controller' import ProductFormController from './product-form-controller'
import ProductGalleryController from './product-gallery-controller' import ProductGalleryController from './product-gallery-controller'
import QuantityController from './quantity-controller' import QuantityController from './quantity-controller'
import RangeSliderController from './range-slider-controller'
import StarRatingController from './star-rating-controller' import StarRatingController from './star-rating-controller'
import TabsController from './tabs-controller' import TabsController from './tabs-controller'
@@ -16,9 +18,11 @@ export function registerControllers(application) {
application.register('appear', AppearController) application.register('appear', AppearController)
application.register('back-to-top', BackToTopController) application.register('back-to-top', BackToTopController)
application.register('carousel', CarouselController) application.register('carousel', CarouselController)
application.register('dropdown', DropdownController)
application.register('product-form', ProductFormController) application.register('product-form', ProductFormController)
application.register('product-gallery', ProductGalleryController) application.register('product-gallery', ProductGalleryController)
application.register('quantity', QuantityController) application.register('quantity', QuantityController)
application.register('range-slider', RangeSliderController)
application.register('star-rating', StarRatingController) application.register('star-rating', StarRatingController)
application.register('tabs', TabsController) application.register('tabs', TabsController)
} }
@@ -0,0 +1,178 @@
import { Controller } from '@hotwired/stimulus'
// Dual-thumb range slider.
//
// Two real <input type="range"> elements stay authoritative — they carry the
// value, the form data, native keyboard support and the no-JS fallback. On
// connect this controller hides their <label>s and mirrors their state onto a
// presentational track: a baseline, a filled span between the two carets, and
// the carets themselves, all positioned with the --min / --max percentage
// custom properties written on the track element.
//
// Pointer drag moves the carets (writing back to the inputs); the keyboard
// drives the inputs directly. Values can't cross — min stays one step below
// max and vice versa. Emits `range-slider:input` while dragging and
// `range-slider:change` on commit, both with { min, max }.
export default class extends Controller {
static targets = ['minInput', 'maxInput', 'field', 'track', 'minThumb', 'maxThumb', 'output', 'reset']
static values = {
min: Number,
max: Number,
step: { type: Number, default: 1 },
prefix: { type: String, default: '' },
suffix: { type: String, default: '' },
separator: { type: String, default: ' – ' },
}
connect() {
this.#clamp()
this.defaults = { min: this.#lo, max: this.#hi }
this.fieldTargets.forEach((field) => field.classList.add('sr-only'))
this.#render()
}
disconnect() {
this.#stopDrag()
}
// ── keyboard / programmatic ──────────────────────────────────────
onInput(event) {
this.#clamp(this.#side(event.target))
this.#render()
this.#emit('input')
}
onChange(event) {
this.#clamp(this.#side(event.target))
this.#render()
this.#emit('change')
}
// The real inputs are visually hidden, so mirror their focus ring onto
// the matching caret to keep a visible focus indicator for keyboard use.
syncFocus(event) {
const thumb = event.target === this.minInputTarget ? this.minThumbTarget : this.maxThumbTarget
thumb.classList.toggle('ring-2', event.type === 'focus')
thumb.classList.toggle('ring-black', event.type === 'focus')
}
// ── pointer drag ────────────────────────────────────────────────────
thumbPointerDown(event) {
const input = event.currentTarget === this.minThumbTarget ? this.minInputTarget : this.maxInputTarget
this.#startDrag(event, input)
}
trackPointerDown(event) {
if (event.target.closest('button')) return // a caret handles its own press
const value = this.#valueAt(event.clientX)
const input = Math.abs(value - this.#lo) <= Math.abs(value - this.#hi)
? this.minInputTarget
: this.maxInputTarget
input.value = value
this.#clamp(this.#side(input))
this.#render()
this.#startDrag(event, input)
}
reset() {
this.minInputTarget.value = this.defaults.min
this.maxInputTarget.value = this.defaults.max
this.#render()
this.#emit('change')
}
// ── internals ──────────────────────────────────────────────────────
#startDrag(event, input) {
event.preventDefault()
this.#stopDrag()
const side = this.#side(input)
this.#onMove = (e) => {
input.value = this.#valueAt(e.clientX)
this.#clamp(side)
this.#render()
this.#emit('input')
}
this.#onUp = () => {
this.#stopDrag()
this.#emit('change')
}
window.addEventListener('pointermove', this.#onMove)
window.addEventListener('pointerup', this.#onUp)
}
#side(input) {
return input === this.maxInputTarget ? 'max' : 'min'
}
#stopDrag() {
if (this.#onMove) window.removeEventListener('pointermove', this.#onMove)
if (this.#onUp) window.removeEventListener('pointerup', this.#onUp)
this.#onMove = this.#onUp = null
}
get #lo() { return Number(this.minInputTarget.value) }
get #hi() { return Number(this.maxInputTarget.value) }
// Keep both thumbs inside the group bounds and stop them crossing. When a
// thumb is being moved (`side`), only that one gives way, so the other
// stays put instead of being dragged along.
#clamp(side = null) {
const gap = this.stepValue
let lo = Math.max(this.minValue, Math.min(this.maxValue, Number(this.minInputTarget.value)))
let hi = Math.max(this.minValue, Math.min(this.maxValue, Number(this.maxInputTarget.value)))
if (side === 'max') hi = Math.max(hi, lo + gap)
else if (side === 'min') lo = Math.min(lo, hi - gap)
else if (lo > hi - gap) lo = hi - gap
this.minInputTarget.value = lo
this.maxInputTarget.value = hi
}
#valueAt(clientX) {
const rect = this.trackTarget.getBoundingClientRect()
const ratio = rect.width ? Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) : 0
const raw = this.minValue + ratio * (this.maxValue - this.minValue)
const step = this.stepValue
return Math.round(raw / step) * step
}
#percent(value) {
const span = this.maxValue - this.minValue
return span ? ((value - this.minValue) / span) * 100 : 0
}
#render() {
const lo = this.#lo
const hi = this.#hi
this.trackTarget.style.setProperty('--min', `${this.#percent(lo)}%`)
this.trackTarget.style.setProperty('--max', `${this.#percent(hi)}%`)
if (this.hasOutputTarget) {
const fmt = (v) => `${this.prefixValue}${v}${this.suffixValue}`
this.outputTarget.textContent = fmt(lo) + this.separatorValue + fmt(hi)
}
if (this.hasResetTarget) {
this.resetTarget.hidden = lo === this.defaults.min && hi === this.defaults.max
}
}
#emit(name) {
const detail = { min: this.#lo, max: this.#hi }
const key = `${detail.min},${detail.max}`
if (name === 'input' && key === this.#lastInputKey) return // no change since last frame
this.#lastInputKey = key
this.dispatch(name, { detail })
}
#onMove = null
#onUp = null
#lastInputKey = null
}
+56 -41
View File
@@ -4,10 +4,10 @@
@section('description', strip_tags($collection['description'] ?? '')) @section('description', strip_tags($collection['description'] ?? ''))
@section('content') @section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-8 py-12"> <div class="max-w-5xl mx-auto pt-26 pb-12">
<div class="flex items-end justify-between gap-6 flex-wrap mb-10"> <div class="flex items-end justify-between gap-6 flex-wrap mb-16">
<h1 class="font-display font-extrabold text-h1">{{ $collection['name'] }}</h1> <h1 class="font-medium text-h2">{{ $collection['name'] }}</h1>
<x-breadcrumb :items="[ <x-breadcrumb :items="[
['label' => __('storefront.nav.home'), 'href' => route('home')], ['label' => __('storefront.nav.home'), 'href' => route('home')],
@@ -15,33 +15,49 @@
]" /> ]" />
</div> </div>
<div class="flex items-center justify-between gap-6 flex-wrap border-b border-black pb-6 mb-10">
<p class="text-neutral-600">
{{ trans_choice('storefront.shop.showing_results', $products->total(), [
'first' => $products->firstItem() ?? 0,
'last' => $products->lastItem() ?? 0,
'total' => $products->total(),
]) }}
</p>
{{-- Dummy — not wired to real sorting yet --}} <div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
<x-ui.select
:ariaLabel="__('storefront.shop.sort_label')" <div class="flex items-center justify-between gap-6 flex-wrap mb-7">
:options="[ {{-- <p class="text-neutral-600"> --}}
['value' => 'default', 'label' => __('storefront.shop.sort_default')], <p>
['value' => 'popularity', 'label' => __('storefront.shop.sort_popularity')], {{ trans_choice('storefront.shop.showing_results', $products->total(), [
['value' => 'price-asc', 'label' => __('storefront.shop.sort_price_asc')], 'first' => $products->firstItem() ?? 0,
['value' => 'price-desc', 'label' => __('storefront.shop.sort_price_desc')], 'last' => $products->lastItem() ?? 0,
['value' => 'newest', 'label' => __('storefront.shop.sort_newest')], 'total' => $products->total(),
]" ]) }}
value="default" </p>
/>
{{-- Dummy — not wired to real sorting yet. Options carry data-action
for a future `category` Stimulus controller; no-op until that
controller exists and a [data-controller="category"] wraps this. --}}
<x-ui.dropdown
id="category-sort"
:label="__('storefront.shop.sort_popularity')"
:ariaLabel="__('storefront.shop.sort_label')"
triggerClass="min-w-48"
>
@foreach ([
'popularity' => __('storefront.shop.sort_popularity'),
'price-asc' => __('storefront.shop.sort_price_asc'),
'price-desc' => __('storefront.shop.sort_price_desc'),
'newest' => __('storefront.shop.sort_newest'),
] as $value => $label)
<x-ui.dropdown.item
data-action="category#sort"
data-category-sort-param="{{ $value }}"
:current="$value === 'popularity'"
>{{ $label }}</x-ui.dropdown.item>
@endforeach
</x-ui.dropdown>
</div>
<div></div>
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
<div class="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-12">
{{-- Products --}} {{-- Products --}}
<div> <div>
@if($products->isEmpty()) @if($products->isEmpty())
<p class="text-neutral-500">{{ __('storefront.shop.no_products') }}</p> <p class="text-neutral-500">{{ __('storefront.shop.no_products') }}</p>
@else @else
@@ -56,7 +72,7 @@
{{-- Sidebar — search, price and availability filters are dummy for now --}} {{-- Sidebar — search, price and availability filters are dummy for now --}}
<aside class="flex flex-col gap-10"> <aside class="flex flex-col gap-10">
<div> <div class="-mt-2">
<label for="shop-search" class="sr-only">{{ __('storefront.shop.search_label') }}</label> <label for="shop-search" class="sr-only">{{ __('storefront.shop.search_label') }}</label>
<div class="relative"> <div class="relative">
<x-ui.input <x-ui.input
@@ -76,24 +92,23 @@ class="absolute right-0 top-1/2 -translate-y-1/2"
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<h2 class="font-display font-extrabold uppercase">{{ __('storefront.shop.filter_price') }}</h2> <p class="font-extrabold text-h4">{{ __('storefront.shop.filter_price') }}</p>
<div class="flex items-center gap-2" aria-hidden="true"> {{-- Dummy — not wired to real price bounds or filtering yet. --}}
<x-ui.icon name="arrow-left" :size="20" /> <x-ui.range-slider
<span class="flex-1 h-px bg-black"></span> :min="10"
<x-ui.icon name="arrow-right" :size="20" /> :max="50"
</div> prefix="€"
separator=" - "
<div class="flex items-center justify-between gap-4"> :legend="__('storefront.shop.filter_price')"
<span class="text-sm">€10 - €50</span> :min-label="__('storefront.shop.price_min')"
<button type="button" class="underline-slide [--slide-h:1px] font-display font-bold italic uppercase text-sm"> :max-label="__('storefront.shop.price_max')"
{{ __('storefront.shop.apply') }} :reset-label="__('storefront.shop.reset')"
</button> />
</div>
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4 [&_label]:text-base">
<h2 class="font-display font-extrabold uppercase">{{ __('storefront.shop.availability') }}</h2> <p class="font-extrabold text-h4">{{ __('storefront.shop.availability') }}</p>
<x-ui.checkbox id="shop-in-stock" name="in_stock"> <x-ui.checkbox id="shop-in-stock" name="in_stock">
{{ __('storefront.shop.in_stock_only') }} {{ __('storefront.shop.in_stock_only') }}
</x-ui.checkbox> </x-ui.checkbox>
@@ -17,7 +17,7 @@
<h2 class="font-display font-extrabold text-h2 mb-10">{{ $title }}</h2> <h2 class="font-display font-extrabold text-h2 mb-10">{{ $title }}</h2>
@endif @endif
<div class="grid {{ $gridCols }} gap-6"> <div class="grid {{ $gridCols }} gap-9">
@foreach($products as $product) @foreach($products as $product)
<x-ui.product-card <x-ui.product-card
:name="$product['name']" :name="$product['name']"
@@ -0,0 +1,42 @@
@props([
'id',
'label' => null,
'ariaLabel' => null,
'triggerClass' => '',
])
<div
data-controller="dropdown"
{{ $attributes->merge(['class' => 'dropdown relative inline-flex items-center']) }}
>
{{-- Popover invoker buttons get implicit aria-expanded / aria-details from
the browser, so only the accessible name needs setting here.
click->dropdown#position measures this button and feeds the panel's
position to CSS before the native popover toggle paints it. --}}
<button
type="button"
popovertarget="{{ $id }}"
popovertargetaction="toggle"
data-dropdown-target="trigger"
data-action="click->dropdown#position"
@if($ariaLabel) aria-label="{{ $ariaLabel }}" @endif
class="bg-transparent border-b border-black pr-10 py-2.5 cursor-pointer focus:outline-none text-left whitespace-nowrap {{ $triggerClass }}"
>
{{ $label }}
</button>
<x-ui.icon
name="arrow-down"
:size="16"
class="dropdown-caret pointer-events-none absolute right-0 transition-transform duration-200"
/>
<div
id="{{ $id }}"
popover
data-dropdown-target="panel"
class="dropdown-panel absolute w-max max-w-xs bg-neutral-200 border border-black"
>
{{ $slot }}
</div>
</div>
@@ -0,0 +1,33 @@
@aware(['id'])
@props([
'href' => null,
'current' => false,
'close' => true,
])
{{--
A single option inside <x-ui.dropdown>.
- Renders a <button> by default, or an <a> when :href is given.
- `close` (button only): also dismiss the panel on click via
popovertargetaction="hide". Any data-action on the same button still
fires — Stimulus handler runs AND the popover closes. Pass :close="false"
for options that shouldn't close the panel (e.g. a multi-select filter).
- `current`: marks the active option (aria-current + bold).
- Everything else (data-action, data-*-param, aria-*, class, …) is forwarded.
`id` comes from the parent <x-ui.dropdown> via @aware.
--}}
@php $tag = $href ? 'a' : 'button'; @endphp
<{{ $tag }}
@if($tag === 'button') type="button" @endif
@if($href) href="{{ $href }}" @endif
@if($close && $tag === 'button') popovertarget="{{ $id }}" popovertargetaction="hide" @endif
@if($current) aria-current="true" @endif
{{ $attributes->merge(['class' => 'block w-full text-left px-5 py-2.5 cursor-pointer transition-colors hover:bg-black hover:text-neutral-200 aria-[current=true]:font-bold']) }}
>
{{ $slot }}
</{{ $tag }}>
@@ -0,0 +1,122 @@
@props([
'min',
'max',
'minValue' => null,
'maxValue' => null,
'step' => 1,
'name' => null,
'legend' => 'Range',
'minLabel' => 'Minimum',
'maxLabel' => 'Maximum',
'prefix' => '',
'suffix' => '',
'separator' => ' – ',
'resetLabel' => 'Reset',
])
@php
$minValue ??= $min;
$maxValue ??= $max;
$uid = 'rs-' . uniqid();
@endphp
<div
data-controller="range-slider"
data-range-slider-min-value="{{ $min }}"
data-range-slider-max-value="{{ $max }}"
data-range-slider-step-value="{{ $step }}"
data-range-slider-prefix-value="{{ $prefix }}"
data-range-slider-suffix-value="{{ $suffix }}"
data-range-slider-separator-value="{{ $separator }}"
{{ $attributes->merge(['class' => 'flex flex-col gap-4']) }}
>
<fieldset class="m-0 min-w-0 border-0 p-0">
<legend class="sr-only">{{ $legend }}</legend>
{{-- Real controls — keyboard, assistive tech, form values, no-JS
fallback. The controller adds `sr-only` to each on connect. --}}
<label data-range-slider-target="field" class="flex items-center gap-2 text-sm">
<span>{{ $minLabel }}</span>
<input
type="range"
id="{{ $uid }}-min"
min="{{ $min }}"
max="{{ $max }}"
step="{{ $step }}"
value="{{ $minValue }}"
@if($name) name="{{ $name }}[min]" @endif
data-range-slider-target="minInput"
data-action="input->range-slider#onInput change->range-slider#onChange focus->range-slider#syncFocus blur->range-slider#syncFocus"
>
</label>
<label data-range-slider-target="field" class="flex items-center gap-2 text-sm">
<span>{{ $maxLabel }}</span>
<input
type="range"
id="{{ $uid }}-max"
min="{{ $min }}"
max="{{ $max }}"
step="{{ $step }}"
value="{{ $maxValue }}"
@if($name) name="{{ $name }}[max]" @endif
data-range-slider-target="maxInput"
data-action="input->range-slider#onInput change->range-slider#onChange focus->range-slider#syncFocus blur->range-slider#syncFocus"
>
</label>
{{-- Presentational track — pointer control + visual state only; the
real controls above own accessibility. --min / --max start at
the extremes so first paint has no jump. --}}
<div
data-range-slider-target="track"
data-action="pointerdown->range-slider#trackPointerDown"
aria-hidden="true"
class="relative h-8 touch-none select-none [--min:0%] [--max:100%]"
>
<span class="pointer-events-none absolute inset-x-0 top-1/2 h-1.25 -translate-y-1/2 bg-neutral-500"></span>
<span class="pointer-events-none absolute top-1/2 left-[var(--min)] right-[calc(100%_-_var(--max))] h-1.25 -translate-y-1/2 bg-black"></span>
<button
type="button"
tabindex="-1"
data-range-slider-target="minThumb"
data-action="pointerdown->range-slider#thumbPointerDown"
class="absolute top-1/2 left-[var(--min)] grid h-8 w-6 -translate-x-1/4 -translate-y-1/2 cursor-grab place-items-center text-black active:cursor-grabbing"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 32" fill="none" aria-hidden="true" class="h-[32px] w-[22px]">
<path d="M17 4 L6 16 L17 28" stroke="currentColor" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" />
</svg>
</button>
<button
type="button"
tabindex="-1"
data-range-slider-target="maxThumb"
data-action="pointerdown->range-slider#thumbPointerDown"
class="absolute top-1/2 left-[var(--max)] grid h-8 w-6 -translate-x-1/2 -translate-y-1/2 cursor-grab place-items-center text-black active:cursor-grabbing"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 32" fill="none" aria-hidden="true" class="h-[32px] w-[22px]">
<path d="M5 4 L16 16 L5 28" stroke="currentColor" stroke-width="5" stroke-linecap="square" stroke-linejoin="miter" />
</svg>
</button>
</div>
</fieldset>
<div class="flex items-center justify-between gap-4">
{{-- Server-rendered so it's right on first paint and without JS; the
controller rewrites it as the values change. --}}
<span data-range-slider-target="output" aria-hidden="true" class="text-sm tabular-nums"
>{{ $prefix }}{{ $minValue }}{{ $suffix }}{{ $separator }}{{ $prefix }}{{ $maxValue }}{{ $suffix }}</span>
<button
type="button"
hidden
data-range-slider-target="reset"
data-action="range-slider#reset"
class="underline-slide [--slide-h:1px] font-display text-sm font-bold uppercase italic"
>{{ $resetLabel }}</button>
</div>
</div>
+1 -10
View File
@@ -5,21 +5,12 @@
'ariaLabel' => null, 'ariaLabel' => null,
]) ])
{{--
Native <select>, styled to match the site's bordered/uppercase look.
Usage:
<x-ui.select
:options="[['value' => 'a', 'label' => 'Option A'], ...]"
value="a"
ariaLabel="Sort products"
/>
--}}
<div class="relative inline-flex items-center"> <div class="relative inline-flex items-center">
<select <select
@if($name) name="{{ $name }}" @endif @if($name) name="{{ $name }}" @endif
@if($ariaLabel) aria-label="{{ $ariaLabel }}" @endif @if($ariaLabel) aria-label="{{ $ariaLabel }}" @endif
{{ $attributes->merge(['class' => 'appearance-none bg-transparent border border-black pl-4 pr-10 py-2.5 font-display font-bold cursor-pointer focus:outline-none']) }} {{ $attributes->merge(['class' => 'appearance-none bg-transparent border-b border-black pr-10 py-2.5 cursor-pointer focus:outline-none']) }}
> >
@foreach($options as $option) @foreach($options as $option)
<option value="{{ $option['value'] }}" @selected($value === $option['value'])>{{ $option['label'] }}</option> <option value="{{ $option['value'] }}" @selected($value === $option['value'])>{{ $option['label'] }}</option>