generated from boboko/starter
category page filters components and general ui - no functionality yet
This commit is contained in:
@@ -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>
|
||||
@@ -5,21 +5,12 @@
|
||||
'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">
|
||||
<select
|
||||
@if($name) name="{{ $name }}" @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)
|
||||
<option value="{{ $option['value'] }}" @selected($value === $option['value'])>{{ $option['label'] }}</option>
|
||||
|
||||
Reference in New Issue
Block a user