Refine filters

This commit is contained in:
2026-06-06 19:36:44 +02:00
parent ed8589a972
commit 3ce3356064
8 changed files with 993 additions and 2338 deletions
+171 -63
View File
@@ -1470,15 +1470,9 @@
.h-full {
height: 100%;
}
.max-h-40 {
max-height: calc(var(--spacing) * 40);
}
.max-h-full {
max-height: 100%;
}
.min-h-\[28px\] {
min-height: 28px;
}
.min-h-screen {
min-height: 100vh;
}
@@ -1655,6 +1649,9 @@
.flex-shrink-0 {
flex-shrink: 0;
}
.shrink-0 {
flex-shrink: 0;
}
.-translate-x-full {
--tw-translate-x: -100%;
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1713,9 +1710,6 @@
.list-disc {
list-style-type: disc;
}
.appearance-none {
appearance: none;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@@ -2125,6 +2119,9 @@
.bg-neutral-primary-soft {
background-color: var(--color-neutral-primary-soft);
}
.bg-neutral-quaternary {
background-color: var(--color-neutral-quaternary);
}
.bg-neutral-secondary-medium {
background-color: var(--color-neutral-secondary-medium);
}
@@ -2331,9 +2328,6 @@
color: heading !important;
}
}
.pb-1 {
padding-bottom: calc(var(--spacing) * 1);
}
.pb-16 {
padding-bottom: calc(var(--spacing) * 16);
}
@@ -3074,12 +3068,6 @@
color: var(--color-blue-700);
}
}
.focus\:ring-0 {
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
}
.focus\:ring-2 {
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
@@ -3895,51 +3883,6 @@
}
}
}
.\[\&\:\:-webkit-slider-thumb\]\:relative {
&::-webkit-slider-thumb {
position: relative;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:z-10 {
&::-webkit-slider-thumb {
z-index: 10;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:z-20 {
&::-webkit-slider-thumb {
z-index: 20;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:h-4 {
&::-webkit-slider-thumb {
height: calc(var(--spacing) * 4);
}
}
.\[\&\:\:-webkit-slider-thumb\]\:w-4 {
&::-webkit-slider-thumb {
width: calc(var(--spacing) * 4);
}
}
.\[\&\:\:-webkit-slider-thumb\]\:cursor-pointer {
&::-webkit-slider-thumb {
cursor: pointer;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:appearance-none {
&::-webkit-slider-thumb {
appearance: none;
}
}
.\[\&\:\:-webkit-slider-thumb\]\:rounded-full {
&::-webkit-slider-thumb {
border-radius: calc(infinity * 1px);
}
}
.\[\&\:\:-webkit-slider-thumb\]\:bg-brand {
&::-webkit-slider-thumb {
background-color: var(--color-brand);
}
}
.\[\&\:first-of-type_button\]\:rounded-s-lg {
&:first-of-type button {
border-start-start-radius: var(--radius-lg);
@@ -4358,6 +4301,171 @@ form input:disabled, select:disabled, textarea:disabled {
padding: calc(var(--spacing) * 4);
}
}
.sf-container {
border-radius: var(--radius-base);
border-style: var(--tw-border-style);
border-width: 1px;
border-color: var(--color-default-medium);
background-color: var(--color-neutral-secondary-medium);
}
.sf-selected {
display: flex;
min-height: 2rem;
flex-wrap: wrap;
gap: calc(var(--spacing) * 1);
padding: calc(var(--spacing) * 2);
}
.sf-tag {
display: inline-flex;
align-items: center;
gap: calc(var(--spacing) * 1);
border-radius: var(--radius);
background-color: color-mix(in srgb, oklch(48.8% 0.243 264.376) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
}
padding-inline: calc(var(--spacing) * 2);
padding-block: calc(var(--spacing) * 0.5);
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-heading);
}
.sf-tag.sf-excluded {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-500) 15%, transparent);
}
color: var(--color-red-600);
text-decoration-line: line-through;
text-decoration-color: var(--color-red-400);
}
.sf-remove {
margin-left: calc(var(--spacing) * 1);
cursor: pointer;
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
color: var(--color-body);
&:hover {
@media (hover: hover) {
color: var(--color-heading);
}
}
}
.sf-modifier-tag {
display: inline-flex;
cursor: pointer;
align-items: center;
border-radius: var(--radius);
background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent);
}
padding-inline: calc(var(--spacing) * 2);
padding-block: calc(var(--spacing) * 0.5);
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-amber-600);
}
.sf-search {
display: block;
width: 100%;
border-style: var(--tw-border-style);
border-width: 0px;
border-top-style: var(--tw-border-style);
border-top-width: 1px;
border-color: var(--color-default-medium);
background-color: transparent;
padding: calc(var(--spacing) * 2);
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-heading);
&:focus {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
--tw-outline-style: none;
outline-style: none;
@media (forced-colors: active) {
outline: 2px solid transparent;
outline-offset: 2px;
}
}
}
.sf-options {
max-height: calc(var(--spacing) * 40);
overflow-y: auto;
padding: calc(var(--spacing) * 1);
color: var(--color-body);
}
.sf-option {
display: flex;
cursor: pointer;
align-items: center;
justify-content: space-between;
border-radius: var(--radius);
padding-inline: calc(var(--spacing) * 2);
padding-block: calc(var(--spacing) * 1);
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
&:hover {
@media (hover: hover) {
background-color: var(--color-neutral-secondary-strong);
}
}
}
.sf-option-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sf-option-buttons {
margin-left: calc(var(--spacing) * 2);
display: flex;
flex-shrink: 0;
gap: calc(var(--spacing) * 1);
}
.sf-btn-include, .sf-btn-exclude {
display: flex;
height: calc(var(--spacing) * 5);
width: calc(var(--spacing) * 5);
align-items: center;
justify-content: center;
border-radius: var(--radius);
border-style: var(--tw-border-style);
border-width: 1px;
border-color: var(--color-default-medium);
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
&:hover {
@media (hover: hover) {
border-color: var(--color-brand);
}
}
&:hover {
@media (hover: hover) {
background-color: var(--color-brand);
}
}
&:hover {
@media (hover: hover) {
color: var(--color-white);
}
}
}
.sf-modifier-option {
cursor: pointer;
padding-inline: calc(var(--spacing) * 2);
padding-block: calc(var(--spacing) * 1);
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
color: var(--color-body);
&:hover {
@media (hover: hover) {
background-color: var(--color-neutral-secondary-strong);
}
}
}
@layer base {
input:where([type='text']),input:where(:not([type])),input:where([type='email']),input:where([type='url']),input:where([type='password']),input:where([type='number']),input:where([type='date']),input:where([type='datetime-local']),input:where([type='month']),input:where([type='search']),input:where([type='tel']),input:where([type='time']),input:where([type='week']),select:where([multiple]),textarea,select {
appearance: none;
+1 -13
View File
@@ -46,9 +46,6 @@
* Returns a plain object ready for JSON.stringify.
*/
function buildFilterJSON(form) {
// Read all SelectableFilter widgets first
readSelectableFilters(form);
var filter = {};
var yearMin = numberValue(form, "filter-year-min");
var yearMax = numberValue(form, "filter-year-max");
@@ -132,14 +129,7 @@
}
if (yearMin !== "" && yearMax !== "") {
// Skip if both equal the data range extremes (no real filter)
var yrMinNum = parseInt(yearMin, 10);
var yrMaxNum = parseInt(yearMax, 10);
if (yrMinNum === yrMaxNum) {
// don't add filter
} else {
filter.year_released = criterion(yearMin, yearMax, "BETWEEN");
}
filter.year_released = criterion(yearMin, yearMax, "BETWEEN");
} else if (yearMin !== "") {
filter.year_released = criterion(yearMin, null, "GREATER_THAN");
} else if (yearMax !== "") {
@@ -371,8 +361,6 @@
}
});
}
injectSearchInputs();
document.addEventListener("DOMContentLoaded", function () {
injectSearchInputs();
loadPresets();
+142 -42
View File
@@ -1,5 +1,12 @@
/**
* Dual-handle range slider — pure JS with draggable handles.
* Range slider — custom draggable handles (no native <input type=range>).
*
* Supports two modes on each slider, toggled via the .range-mode-toggle button:
* range (default) — two handles, min ≤ max constraint
* point — single handle, sets both number inputs to the same value
*
* Handles track-fill positioning and sync between handles and the connected
* number inputs (linked via data-target attributes).
*/
(function () {
"use strict";
@@ -10,63 +17,109 @@
if (slider._rsInit) return;
slider._rsInit = true;
var mode = slider.getAttribute("data-mode") || "range";
var trackFill = slider.querySelector(".range-track-fill");
var minHandle = slider.querySelector(".range-handle-min");
var maxHandle = slider.querySelector(".range-handle-max");
var track = slider.querySelector(".range-track-fill");
if (!minHandle || !maxHandle) return;
var minTarget = document.getElementById(minHandle.getAttribute("data-target"));
var maxTarget = document.getElementById(maxHandle.getAttribute("data-target"));
var dMin = parseInt(slider.getAttribute("data-min"), 10);
var dMax = parseInt(slider.getAttribute("data-max"), 10);
var minTarget = document.getElementById(
minHandle.getAttribute("data-target")
);
var maxTarget = document.getElementById(
maxHandle.getAttribute("data-target")
);
var dataMin = parseInt(slider.getAttribute("data-min"), 10);
var dataMax = parseInt(slider.getAttribute("data-max"), 10);
var step = parseInt(slider.getAttribute("data-step"), 10) || 1;
function valueToPercent(v) { return ((v - dMin) / (dMax - dMin)) * 100; }
function percentToValue(p) {
var raw = dMin + (p / 100) * (dMax - dMin);
// ── Helpers ──
function valueToPercent(value) {
return ((value - dataMin) / (dataMax - dataMin)) * 100;
}
function percentToValue(percent) {
var raw = dataMin + (percent / 100) * (dataMax - dataMin);
return Math.round(raw / step) * step;
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function clamp(value, lo, hi) {
return Math.max(lo, Math.min(hi, value));
}
function getTargetVal(el) { return parseInt(el ? el.value : minTarget.value, 10) || dMin; }
function setTargetVal(el, v) { if (el) el.value = v; }
function getTargetValue(target) {
return parseInt(target ? target.value : 0, 10) || dataMin;
}
function setTargetValue(target, value) {
if (target) target.value = value;
}
function update() {
var minV = getTargetVal(minTarget);
var maxV = getTargetVal(maxTarget);
minV = clamp(minV, dMin, dMax);
maxV = clamp(maxV, dMin, dMax);
if (minV > maxV) minV = maxV;
if (maxV < minV) maxV = minV;
setTargetVal(minTarget, minV);
setTargetVal(maxTarget, maxV);
var minP = valueToPercent(minV);
var maxP = valueToPercent(maxV);
minHandle.style.left = minP + "%";
maxHandle.style.left = maxP + "%";
if (track) {
track.style.left = minP + "%";
track.style.width = (maxP - minP) + "%";
// ── Track fill positioning ──
function updateTrackFill() {
if (!trackFill) return;
var minValue = getTargetValue(minTarget);
var maxValue = getTargetValue(maxTarget);
if (mode === "point") {
trackFill.style.left = "0%";
trackFill.style.width = valueToPercent(maxValue) + "%";
} else {
var leftPct = valueToPercent(minValue);
var widthPct = valueToPercent(maxValue) - leftPct;
trackFill.style.left = leftPct + "%";
trackFill.style.width = widthPct + "%";
}
}
function updateHandles() {
minHandle.style.left = valueToPercent(getTargetValue(minTarget)) + "%";
maxHandle.style.left = valueToPercent(getTargetValue(maxTarget)) + "%";
updateTrackFill();
}
// ── Dragging ──
function makeDraggable(handle, isMin) {
handle.addEventListener("mousedown", function (e) {
e.preventDefault();
var rect = slider.getBoundingClientRect();
function onMove(ev) {
var pct = ((ev.clientX - rect.left) / rect.width) * 100;
var v = percentToValue(clamp(pct, 0, 100));
if (isMin) {
minTarget.value = clamp(v, dMin, getTargetVal(maxTarget));
var value = percentToValue(clamp(pct, 0, 100));
if (mode === "point") {
setTargetValue(minTarget, value);
setTargetValue(maxTarget, value);
if (minTarget)
minTarget.dispatchEvent(
new Event("input", { bubbles: true })
);
if (maxTarget)
maxTarget.dispatchEvent(
new Event("input", { bubbles: true })
);
} else if (isMin) {
setTargetValue(
minTarget,
clamp(value, dataMin, getTargetValue(maxTarget))
);
if (minTarget)
minTarget.dispatchEvent(
new Event("input", { bubbles: true })
);
} else {
maxTarget.value = clamp(v, getTargetVal(minTarget), dMax);
setTargetValue(
maxTarget,
clamp(value, getTargetValue(minTarget), dataMax)
);
if (maxTarget)
maxTarget.dispatchEvent(
new Event("input", { bubbles: true })
);
}
update();
// Trigger input event on the target so any listeners fire
var tgt = isMin ? minTarget : maxTarget;
if (tgt) tgt.dispatchEvent(new Event("input", { bubbles: true }));
updateHandles();
}
function onUp() {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
@@ -80,17 +133,64 @@
makeDraggable(minHandle, true);
makeDraggable(maxHandle, false);
// Sync from inputs to slider
function fromInputs() { update(); }
if (minTarget) minTarget.addEventListener("input", fromInputs);
if (maxTarget) maxTarget.addEventListener("input", fromInputs);
// ── Sync from number inputs back to handles ──
update();
function syncFromInputs() {
if (mode === "point") {
var value =
getTargetValue(minTarget) || getTargetValue(maxTarget);
setTargetValue(minTarget, value);
setTargetValue(maxTarget, value);
}
updateHandles();
}
if (minTarget)
minTarget.addEventListener("input", syncFromInputs);
if (maxTarget)
maxTarget.addEventListener("input", syncFromInputs);
// ── Mode toggle ──
var block = slider.closest(".range-slider-block");
var toggleButton =
block && block.querySelector(".range-mode-toggle");
if (toggleButton) {
toggleButton.addEventListener("click", function () {
var newMode = mode === "range" ? "point" : "range";
slider.setAttribute("data-mode", newMode);
// Swap toggle icons
var iconRange = toggleButton.querySelector(
".range-mode-icon-range"
);
var iconPoint = toggleButton.querySelector(
".range-mode-icon-point"
);
if (iconRange) iconRange.classList.toggle("hidden");
if (iconPoint) iconPoint.classList.toggle("hidden");
var dashSpan = block && block.querySelector(".range-dash");
if (newMode === "point") {
minHandle.style.display = "none";
setTargetValue(minTarget, getTargetValue(maxTarget));
if (minTarget) minTarget.classList.add("hidden");
if (dashSpan) dashSpan.classList.add("hidden");
} else {
minHandle.style.display = "";
if (minTarget) minTarget.classList.remove("hidden");
if (dashSpan) dashSpan.classList.remove("hidden");
}
mode = newMode;
updateHandles();
});
}
// ── Initial position ──
updateHandles();
});
}
document.addEventListener("DOMContentLoaded", initAll);
document.addEventListener("htmx:afterSwap", initAll);
// Expose for manual re-init (filter bar toggle)
window.initRangeSliders = initAll;
})();