Improve search select
This commit is contained in:
@@ -26,9 +26,13 @@ class SearchSelectOption(TypedDict):
|
|||||||
|
|
||||||
# removed border and border-default-medium, see later if it's needed
|
# removed border and border-default-medium, see later if it's needed
|
||||||
_CONTAINER_CLASS = "relative rounded-base bg-neutral-secondary-medium"
|
_CONTAINER_CLASS = "relative rounded-base bg-neutral-secondary-medium"
|
||||||
_PILLS_CLASS = "flex flex-wrap gap-1 p-2 empty:hidden"
|
# The pills and the search box share one flex-wrap row so the widget reads as a
|
||||||
|
# single field; the pills wrapper uses `contents` so its pills/hidden inputs
|
||||||
|
# flow as direct participants of that row, inline with the search input.
|
||||||
|
_FIELD_CLASS = "flex flex-wrap items-center gap-1 p-2"
|
||||||
|
_PILLS_CLASS = "contents"
|
||||||
_SEARCH_CLASS = (
|
_SEARCH_CLASS = (
|
||||||
"block w-full border-0 bg-transparent text-sm text-heading p-2 "
|
"flex-1 min-w-[8rem] border-0 bg-transparent text-sm text-heading "
|
||||||
"focus:ring-0 focus:outline-hidden placeholder:text-body"
|
"focus:ring-0 focus:outline-hidden placeholder:text-body"
|
||||||
)
|
)
|
||||||
_OPTIONS_CLASS = (
|
_OPTIONS_CLASS = (
|
||||||
@@ -99,7 +103,13 @@ def SearchSelect(
|
|||||||
options = [_normalize_option(o) for o in (options or [])]
|
options = [_normalize_option(o) for o in (options or [])]
|
||||||
|
|
||||||
# ── Pills + their hidden inputs (the submitted channel) ──
|
# ── Pills + their hidden inputs (the submitted channel) ──
|
||||||
|
# Multi-select renders a removable Pill per value; single-select renders no
|
||||||
|
# pill — the committed label shows inside the search box instead, with a
|
||||||
|
# lone hidden input carrying the value. Both keep the hidden input(s) inside
|
||||||
|
# `[data-ss-pills]` so the JS reads/writes values uniformly.
|
||||||
pills_children: list[SafeText] = []
|
pills_children: list[SafeText] = []
|
||||||
|
search_value = ""
|
||||||
|
if multi_select:
|
||||||
for option in selected:
|
for option in selected:
|
||||||
pills_children.append(
|
pills_children.append(
|
||||||
Pill(
|
Pill(
|
||||||
@@ -110,6 +120,10 @@ def SearchSelect(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
pills_children.append(_hidden_input(name, option["value"]))
|
pills_children.append(_hidden_input(name, option["value"]))
|
||||||
|
elif selected:
|
||||||
|
option = selected[0]
|
||||||
|
pills_children.append(_hidden_input(name, option["value"]))
|
||||||
|
search_value = option["label"]
|
||||||
|
|
||||||
pills = Component(
|
pills = Component(
|
||||||
tag_name="div",
|
tag_name="div",
|
||||||
@@ -118,15 +132,22 @@ def SearchSelect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ── Search box (NO name — the query is never submitted) ──
|
# ── Search box (NO name — the query is never submitted) ──
|
||||||
search = Component(
|
search_attrs: list[HTMLAttribute] = [
|
||||||
tag_name="input",
|
|
||||||
attributes=[
|
|
||||||
("data-ss-search", ""),
|
("data-ss-search", ""),
|
||||||
("type", "text"),
|
("type", "text"),
|
||||||
("placeholder", placeholder),
|
("placeholder", placeholder),
|
||||||
("autocomplete", "off"),
|
("autocomplete", "off"),
|
||||||
("class", _SEARCH_CLASS),
|
("class", _SEARCH_CLASS),
|
||||||
],
|
]
|
||||||
|
if search_value:
|
||||||
|
search_attrs.append(("value", search_value))
|
||||||
|
search = Component(tag_name="input", attributes=search_attrs)
|
||||||
|
|
||||||
|
# ── Field row: pills + search box combined into one visual field ──
|
||||||
|
field = Component(
|
||||||
|
tag_name="div",
|
||||||
|
attributes=[("data-ss-field", ""), ("class", _FIELD_CLASS)],
|
||||||
|
children=[pills, search],
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Options panel (pre-rendered only when there is no search_url) ──
|
# ── Options panel (pre-rendered only when there is no search_url) ──
|
||||||
@@ -164,7 +185,7 @@ def SearchSelect(
|
|||||||
return Component(
|
return Component(
|
||||||
tag_name="div",
|
tag_name="div",
|
||||||
attributes=container_attrs,
|
attributes=container_attrs,
|
||||||
children=[pills, search, options_panel],
|
children=[field, options_panel],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+6
-179
@@ -293,85 +293,27 @@
|
|||||||
--leading-5: 20px;
|
--leading-5: 20px;
|
||||||
--radius-base: 12px;
|
--radius-base: 12px;
|
||||||
--color-body: var(--color-gray-600);
|
--color-body: var(--color-gray-600);
|
||||||
--color-body-subtle: var(--color-gray-500);
|
|
||||||
--color-heading: var(--color-gray-900);
|
--color-heading: var(--color-gray-900);
|
||||||
--color-fg-brand-subtle: var(--color-blue-200);
|
|
||||||
--color-fg-brand: var(--color-blue-700);
|
--color-fg-brand: var(--color-blue-700);
|
||||||
--color-fg-brand-strong: var(--color-blue-900);
|
|
||||||
--color-fg-success: var(--color-emerald-700);
|
|
||||||
--color-fg-success-strong: var(--color-emerald-900);
|
|
||||||
--color-fg-danger: var(--color-rose-700);
|
|
||||||
--color-fg-danger-strong: var(--color-rose-900);
|
|
||||||
--color-fg-warning-subtle: var(--color-orange-600);
|
|
||||||
--color-fg-warning: var(--color-orange-900);
|
|
||||||
--color-fg-yellow: var(--color-yellow-400);
|
|
||||||
--color-fg-disabled: var(--color-gray-400);
|
--color-fg-disabled: var(--color-gray-400);
|
||||||
--color-fg-purple: var(--color-purple-600);
|
|
||||||
--color-fg-cyan: var(--color-cyan-600);
|
|
||||||
--color-fg-indigo: var(--color-indigo-600);
|
|
||||||
--color-fg-pink: var(--color-pink-600);
|
|
||||||
--color-fg-lime: var(--color-lime-600);
|
|
||||||
--color-neutral-primary-soft: var(--color-white);
|
--color-neutral-primary-soft: var(--color-white);
|
||||||
--color-neutral-primary: var(--color-white);
|
--color-neutral-primary: var(--color-white);
|
||||||
--color-neutral-primary-medium: var(--color-white);
|
--color-neutral-primary-medium: var(--color-white);
|
||||||
--color-neutral-primary-strong: var(--color-white);
|
|
||||||
--color-neutral-secondary-soft: var(--color-gray-50);
|
--color-neutral-secondary-soft: var(--color-gray-50);
|
||||||
--color-neutral-secondary: var(--color-gray-50);
|
--color-neutral-secondary: var(--color-gray-50);
|
||||||
--color-neutral-secondary-medium: var(--color-gray-50);
|
--color-neutral-secondary-medium: var(--color-gray-50);
|
||||||
--color-neutral-secondary-strong: var(--color-gray-50);
|
--color-neutral-secondary-strong: var(--color-gray-50);
|
||||||
--color-neutral-secondary-strongest: var(--color-gray-50);
|
|
||||||
--color-neutral-tertiary-soft: var(--color-gray-100);
|
|
||||||
--color-neutral-tertiary: var(--color-gray-100);
|
--color-neutral-tertiary: var(--color-gray-100);
|
||||||
--color-neutral-tertiary-medium: var(--color-gray-100);
|
--color-neutral-tertiary-medium: var(--color-gray-100);
|
||||||
--color-neutral-quaternary: var(--color-gray-200);
|
--color-neutral-quaternary: var(--color-gray-200);
|
||||||
--color-neutral-quaternary-medium: var(--color-gray-200);
|
|
||||||
--color-gray: var(--color-gray-300);
|
|
||||||
--color-brand-softer: var(--color-blue-50);
|
|
||||||
--color-brand-soft: var(--color-blue-100);
|
--color-brand-soft: var(--color-blue-100);
|
||||||
--color-brand: var(--color-blue-700);
|
--color-brand: var(--color-blue-700);
|
||||||
--color-brand-medium: var(--color-blue-200);
|
--color-brand-medium: var(--color-blue-200);
|
||||||
--color-brand-strong: var(--color-blue-800);
|
--color-brand-strong: var(--color-blue-800);
|
||||||
--color-success-soft: var(--color-emerald-50);
|
|
||||||
--color-success: var(--color-emerald-700);
|
|
||||||
--color-success-medium: var(--color-emerald-100);
|
|
||||||
--color-success-strong: var(--color-emerald-800);
|
|
||||||
--color-danger-soft: var(--color-rose-50);
|
|
||||||
--color-danger: var(--color-rose-700);
|
|
||||||
--color-danger-medium: var(--color-rose-100);
|
|
||||||
--color-danger-strong: var(--color-rose-800);
|
|
||||||
--color-warning-soft: var(--color-orange-50);
|
|
||||||
--color-warning: var(--color-orange-500);
|
|
||||||
--color-warning-medium: var(--color-orange-100);
|
|
||||||
--color-warning-strong: var(--color-orange-700);
|
|
||||||
--color-dark-soft: var(--color-gray-800);
|
|
||||||
--color-dark: var(--color-gray-800);
|
--color-dark: var(--color-gray-800);
|
||||||
--color-dark-strong: var(--color-gray-900);
|
|
||||||
--color-disabled: var(--color-gray-100);
|
|
||||||
--color-purple: var(--color-purple-500);
|
|
||||||
--color-sky: var(--color-sky-500);
|
|
||||||
--color-teal: var(--color-teal-600);
|
|
||||||
--color-pink: var(--color-pink-600);
|
|
||||||
--color-cyan: var(--color-cyan-500);
|
|
||||||
--color-fuchsia: var(--color-fuchsia-600);
|
|
||||||
--color-indigo: var(--color-indigo-600);
|
|
||||||
--color-orange: var(--color-orange-400);
|
|
||||||
--color-buffer: var(--color-white);
|
|
||||||
--color-buffer-medium: var(--color-white);
|
|
||||||
--color-buffer-strong: var(--color-white);
|
|
||||||
--color-muted: var(--color-gray-50);
|
|
||||||
--color-light-subtle: var(--color-gray-100);
|
|
||||||
--color-light: var(--color-gray-100);
|
--color-light: var(--color-gray-100);
|
||||||
--color-light-medium: var(--color-gray-100);
|
|
||||||
--color-default-subtle: var(--color-gray-200);
|
|
||||||
--color-default: var(--color-gray-200);
|
--color-default: var(--color-gray-200);
|
||||||
--color-default-medium: var(--color-gray-200);
|
--color-default-medium: var(--color-gray-200);
|
||||||
--color-default-strong: var(--color-gray-200);
|
|
||||||
--color-success-subtle: var(--color-emerald-200);
|
|
||||||
--color-danger-subtle: var(--color-rose-200);
|
|
||||||
--color-warning-subtle: var(--color-orange-200);
|
|
||||||
--color-brand-subtle: var(--color-blue-200);
|
|
||||||
--color-brand-light: var(--color-blue-600);
|
|
||||||
--color-dark-subtle: var(--color-gray-800);
|
|
||||||
--color-dark-backdrop: var(--color-gray-950);
|
--color-dark-backdrop: var(--color-gray-950);
|
||||||
--color-accent: #7c3aed;
|
--color-accent: #7c3aed;
|
||||||
}
|
}
|
||||||
@@ -878,18 +820,12 @@
|
|||||||
.start-0 {
|
.start-0 {
|
||||||
inset-inline-start: calc(var(--spacing) * 0);
|
inset-inline-start: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.end-1 {
|
|
||||||
inset-inline-end: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.end-1\.5 {
|
.end-1\.5 {
|
||||||
inset-inline-end: calc(var(--spacing) * 1.5);
|
inset-inline-end: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
.top-0 {
|
.top-0 {
|
||||||
top: calc(var(--spacing) * 0);
|
top: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.top-1 {
|
|
||||||
top: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1 / 2 * 100%);
|
top: calc(1 / 2 * 100%);
|
||||||
}
|
}
|
||||||
@@ -911,9 +847,6 @@
|
|||||||
.bottom-0 {
|
.bottom-0 {
|
||||||
bottom: calc(var(--spacing) * 0);
|
bottom: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.bottom-1 {
|
|
||||||
bottom: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.bottom-1\.5 {
|
.bottom-1\.5 {
|
||||||
bottom: calc(var(--spacing) * 1.5);
|
bottom: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
@@ -1482,6 +1415,9 @@
|
|||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
.contents {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@@ -1540,15 +1476,9 @@
|
|||||||
.h-full {
|
.h-full {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.max-h-40 {
|
|
||||||
max-height: calc(var(--spacing) * 40);
|
|
||||||
}
|
|
||||||
.max-h-full {
|
.max-h-full {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
.min-h-\[28px\] {
|
|
||||||
min-height: 28px;
|
|
||||||
}
|
|
||||||
.min-h-screen {
|
.min-h-screen {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
@@ -1617,15 +1547,9 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.w-1 {
|
|
||||||
width: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.w-1\/2 {
|
.w-1\/2 {
|
||||||
width: calc(1 / 2 * 100%);
|
width: calc(1 / 2 * 100%);
|
||||||
}
|
}
|
||||||
.w-2 {
|
|
||||||
width: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.w-2\.5 {
|
.w-2\.5 {
|
||||||
width: calc(var(--spacing) * 2.5);
|
width: calc(var(--spacing) * 2.5);
|
||||||
}
|
}
|
||||||
@@ -1722,6 +1646,9 @@
|
|||||||
border-color: var(--color-brand);
|
border-color: var(--color-brand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.min-w-\[8rem\] {
|
||||||
|
min-width: 8rem;
|
||||||
|
}
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@@ -1734,9 +1661,6 @@
|
|||||||
.shrink-0 {
|
.shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.border-collapse {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
.-translate-x-full {
|
.-translate-x-full {
|
||||||
--tw-translate-x: -100%;
|
--tw-translate-x: -100%;
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -1753,10 +1677,6 @@
|
|||||||
--tw-translate-x: 100%;
|
--tw-translate-x: 100%;
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
}
|
}
|
||||||
.-translate-y-1 {
|
|
||||||
--tw-translate-y: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-y-1\/2 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -1799,9 +1719,6 @@
|
|||||||
.list-disc {
|
.list-disc {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
.appearance-none {
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
.grid-cols-1 {
|
.grid-cols-1 {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -2145,9 +2062,6 @@
|
|||||||
.bg-amber-50 {
|
.bg-amber-50 {
|
||||||
background-color: var(--color-amber-50);
|
background-color: var(--color-amber-50);
|
||||||
}
|
}
|
||||||
.bg-black {
|
|
||||||
background-color: var(--color-black);
|
|
||||||
}
|
|
||||||
.bg-black\/70 {
|
.bg-black\/70 {
|
||||||
background-color: color-mix(in srgb, #000 70%, transparent);
|
background-color: color-mix(in srgb, #000 70%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -2172,9 +2086,6 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
|
background-color: color-mix(in oklab, var(--color-brand) 15%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bg-dark-backdrop {
|
|
||||||
background-color: var(--color-dark-backdrop);
|
|
||||||
}
|
|
||||||
.bg-dark-backdrop\/70 {
|
.bg-dark-backdrop\/70 {
|
||||||
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent);
|
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -2193,18 +2104,12 @@
|
|||||||
.bg-gray-500 {
|
.bg-gray-500 {
|
||||||
background-color: var(--color-gray-500);
|
background-color: var(--color-gray-500);
|
||||||
}
|
}
|
||||||
.bg-gray-800 {
|
|
||||||
background-color: var(--color-gray-800);
|
|
||||||
}
|
|
||||||
.bg-gray-800\/20 {
|
.bg-gray-800\/20 {
|
||||||
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 20%, transparent);
|
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 20%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
background-color: color-mix(in oklab, var(--color-gray-800) 20%, transparent);
|
background-color: color-mix(in oklab, var(--color-gray-800) 20%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bg-gray-900 {
|
|
||||||
background-color: var(--color-gray-900);
|
|
||||||
}
|
|
||||||
.bg-gray-900\/50 {
|
.bg-gray-900\/50 {
|
||||||
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 50%, transparent);
|
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 50%, transparent);
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
@@ -2319,18 +2224,6 @@
|
|||||||
fill: white !important;
|
fill: white !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.apexcharts-gridline {
|
|
||||||
stroke: var(--color-default) !important;
|
|
||||||
.dark & {
|
|
||||||
stroke: var(--color-default) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.apexcharts-xcrosshairs {
|
|
||||||
stroke: var(--color-default) !important;
|
|
||||||
.dark & {
|
|
||||||
stroke: var(--color-default) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.apexcharts-ycrosshairs {
|
.apexcharts-ycrosshairs {
|
||||||
stroke: var(--color-default) !important;
|
stroke: var(--color-default) !important;
|
||||||
.dark & {
|
.dark & {
|
||||||
@@ -2389,9 +2282,6 @@
|
|||||||
.px-6 {
|
.px-6 {
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
.py-0 {
|
|
||||||
padding-block: calc(var(--spacing) * 0);
|
|
||||||
}
|
|
||||||
.py-0\.5 {
|
.py-0\.5 {
|
||||||
padding-block: calc(var(--spacing) * 0.5);
|
padding-block: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
@@ -2453,9 +2343,6 @@
|
|||||||
color: heading !important;
|
color: heading !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pb-1 {
|
|
||||||
padding-bottom: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.pb-16 {
|
.pb-16 {
|
||||||
padding-bottom: calc(var(--spacing) * 16);
|
padding-bottom: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
@@ -2622,9 +2509,6 @@
|
|||||||
.text-balance {
|
.text-balance {
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
.text-wrap {
|
|
||||||
text-wrap: wrap;
|
|
||||||
}
|
|
||||||
.whitespace-nowrap {
|
.whitespace-nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -2751,9 +2635,6 @@
|
|||||||
.italic {
|
.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.no-underline {
|
|
||||||
text-decoration-line: none;
|
|
||||||
}
|
|
||||||
.no-underline\! {
|
.no-underline\! {
|
||||||
text-decoration-line: none !important;
|
text-decoration-line: none !important;
|
||||||
}
|
}
|
||||||
@@ -2817,10 +2698,6 @@
|
|||||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
||||||
}
|
}
|
||||||
.backdrop-filter {
|
|
||||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
|
||||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
|
||||||
}
|
|
||||||
.transition {
|
.transition {
|
||||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
@@ -2985,11 +2862,6 @@
|
|||||||
background-color: var(--color-gray-50);
|
background-color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.empty\:hidden {
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:scale-110 {
|
.hover\:scale-110 {
|
||||||
&:hover {
|
&:hover {
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -4042,51 +3914,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\]\:rounded-s-lg {
|
||||||
&:first-of-type button {
|
&:first-of-type button {
|
||||||
border-start-start-radius: var(--radius-lg);
|
border-start-start-radius: var(--radius-lg);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* SearchSelect widget — a search box paired with a dropdown of options.
|
* SearchSelect widget — a search box paired with a dropdown of options.
|
||||||
* Single/multi select; chosen items render as removable pills, each backed by a
|
* Multi-select renders chosen items as removable pills (inline with the search
|
||||||
* hidden <input> so existing Django form validation keeps working.
|
* box), each backed by a hidden <input>. Single-select renders no pill: the
|
||||||
|
* committed label lives inside the search box (which doubles as a combobox —
|
||||||
|
* focus clears it to search, picking an option fills it), with a lone hidden
|
||||||
|
* <input> carrying the value. Both keep hidden inputs so Django validation works.
|
||||||
*
|
*
|
||||||
* Mirrors selectable_filter.js: initAll() on DOMContentLoaded + htmx:afterSwap,
|
* Mirrors selectable_filter.js: initAll() on DOMContentLoaded + htmx:afterSwap,
|
||||||
* each widget guarded with el._ssInit.
|
* each widget guarded with el._ssInit.
|
||||||
@@ -120,8 +123,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
search.addEventListener("focus", runSearch);
|
// ── Single-select combobox: the search box shows the committed label;
|
||||||
search.addEventListener("input", runSearch);
|
// focusing clears it to search, blurring restores it (or deselects). ──
|
||||||
|
if (!multi) container._ssLabel = search.value;
|
||||||
|
|
||||||
|
search.addEventListener("focus", function () {
|
||||||
|
if (!multi) {
|
||||||
|
// Hide the committed label so the box becomes a fresh search field.
|
||||||
|
search.value = "";
|
||||||
|
container._ssDirty = false;
|
||||||
|
}
|
||||||
|
runSearch();
|
||||||
|
});
|
||||||
|
search.addEventListener("input", function () {
|
||||||
|
if (!multi) container._ssDirty = true;
|
||||||
|
runSearch();
|
||||||
|
});
|
||||||
|
if (!multi) {
|
||||||
|
search.addEventListener("blur", function () {
|
||||||
|
// Defer so an option click (which fires before blur settles) wins.
|
||||||
|
setTimeout(function () {
|
||||||
|
if (container._ssDirty && search.value.trim() === "") {
|
||||||
|
// User intentionally cleared the box → deselect.
|
||||||
|
pills.innerHTML = "";
|
||||||
|
container._ssLabel = "";
|
||||||
|
emitChange(null);
|
||||||
|
} else {
|
||||||
|
// Focused-and-left, or typed a partial query without picking →
|
||||||
|
// restore the committed label (no-op right after a selection).
|
||||||
|
search.value = container._ssLabel || "";
|
||||||
|
}
|
||||||
|
}, 120);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking an option must not blur the input before the click selects.
|
||||||
|
options.addEventListener("mousedown", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Option click → select ──
|
// ── Option click → select ──
|
||||||
options.addEventListener("click", function (e) {
|
options.addEventListener("click", function (e) {
|
||||||
@@ -152,9 +191,13 @@
|
|||||||
addPill(option);
|
addPill(option);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Single-select: no pill — show the label in the search box and keep a
|
||||||
|
// lone hidden input under [data-ss-pills] for submission.
|
||||||
pills.innerHTML = "";
|
pills.innerHTML = "";
|
||||||
addPill(option);
|
pills.appendChild(buildHidden(option.value));
|
||||||
search.value = option.label;
|
search.value = option.label;
|
||||||
|
container._ssLabel = option.label;
|
||||||
|
container._ssDirty = false;
|
||||||
hidePanel();
|
hidePanel();
|
||||||
}
|
}
|
||||||
emitChange(option);
|
emitChange(option);
|
||||||
|
|||||||
@@ -63,9 +63,10 @@ class SearchSelectComponentTest(unittest.TestCase):
|
|||||||
self.assertIn('data-search-url="/api/games/search"', html)
|
self.assertIn('data-search-url="/api/games/search"', html)
|
||||||
self.assertIn('data-multi="true"', html)
|
self.assertIn('data-multi="true"', html)
|
||||||
|
|
||||||
def test_selected_renders_pills_and_hidden_inputs(self):
|
def test_multi_selected_renders_pills_and_hidden_inputs(self):
|
||||||
html = SearchSelect(
|
html = SearchSelect(
|
||||||
name="games",
|
name="games",
|
||||||
|
multi_select=True,
|
||||||
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
||||||
)
|
)
|
||||||
self.assertIn("data-pill", html)
|
self.assertIn("data-pill", html)
|
||||||
@@ -75,6 +76,18 @@ class SearchSelectComponentTest(unittest.TestCase):
|
|||||||
# name. The leading space avoids matching the container's data-name.
|
# name. The leading space avoids matching the container's data-name.
|
||||||
self.assertEqual(html.count(' name="games"'), 1)
|
self.assertEqual(html.count(' name="games"'), 1)
|
||||||
|
|
||||||
|
def test_single_selected_has_no_pill_and_value_in_search_box(self):
|
||||||
|
html = SearchSelect(
|
||||||
|
name="games",
|
||||||
|
selected=[{"value": 7, "label": "Game A", "data": {"platform": "2"}}],
|
||||||
|
)
|
||||||
|
# single-select renders no pill — the label lives in the search box
|
||||||
|
self.assertNotIn("data-pill", html)
|
||||||
|
self.assertIn('value="Game A"', html)
|
||||||
|
# the value is still submitted via a lone hidden input
|
||||||
|
self.assertIn('<input type="hidden" name="games" value="7">', html)
|
||||||
|
self.assertEqual(html.count(' name="games"'), 1)
|
||||||
|
|
||||||
def test_search_box_has_no_name(self):
|
def test_search_box_has_no_name(self):
|
||||||
html = SearchSelect(name="games")
|
html = SearchSelect(name="games")
|
||||||
self.assertIn("data-ss-search", html)
|
self.assertIn("data-ss-search", html)
|
||||||
|
|||||||
Reference in New Issue
Block a user