feat(ui): mejora en el selector de años en PublicationsTable y ajustes en SwordProfileSelect

Se reemplaza el elemento <select> por el componente CustomSelect en PublicationsTable para una mejor experiencia de usuario al seleccionar años. Además, se ajusta el estilo del componente SwordProfileSelect para mejorar la presentación de las opciones. Se asegura que la funcionalidad de selección se mantenga intacta.
This commit is contained in:
Alexis
2026-06-01 13:33:36 +02:00
parent 02c65bb710
commit 552254d4a8
3 changed files with 105 additions and 24 deletions
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { AlertIcon, ChevronDownIcon, FilterIcon, SearchIcon, SparkleIcon } from "../ui/Icons";
import { CustomSelect } from "../ui/CustomSelect";
import { Spinner } from "../ui/Spinner";
import { Badge } from "../ui/Badge";
@@ -314,20 +315,16 @@ export function PublicationsTable({
>
Desde año
</label>
<select
<CustomSelect
id="year-from"
value={yearFrom}
onChange={(e) => handleYearFromChange(e.target.value)}
onChange={handleYearFromChange}
disabled={availableYears.length === 0}
className="rounded-md border border-surface-border-strong bg-surface-primary px-2.5 py-1.5 text-[13px] text-ink-primary outline-none focus:border-brand-accent disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Cualquiera</option>
{availableYears.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
options={availableYears.map((y) => ({
value: String(y),
label: String(y),
}))}
/>
</div>
<div className="flex flex-col gap-1">
<label
@@ -336,20 +333,16 @@ export function PublicationsTable({
>
Hasta año
</label>
<select
<CustomSelect
id="year-to"
value={yearTo}
onChange={(e) => handleYearToChange(e.target.value)}
onChange={handleYearToChange}
disabled={availableYears.length === 0}
className="rounded-md border border-surface-border-strong bg-surface-primary px-2.5 py-1.5 text-[13px] text-ink-primary outline-none focus:border-brand-accent disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Cualquiera</option>
{availableYears.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
options={availableYears.map((y) => ({
value: String(y),
label: String(y),
}))}
/>
</div>
{hasYearFilter && (
<button
@@ -69,7 +69,7 @@ export function SwordProfileSelect({
<div
role="listbox"
aria-labelledby={id}
className="absolute left-0 top-[calc(100%+6px)] z-50 min-w-[260px] overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg"
className="absolute left-0 top-[calc(100%+6px)] z-50 min-w-80 overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg"
>
{options.map(({ value: optionValue, label, desc }, idx) => (
<button
@@ -91,7 +91,9 @@ export function SwordProfileSelect({
<div className="text-sm font-medium text-ink-primary">
{label}
</div>
<div className="text-xs text-ink-tertiary">{desc}</div>
<div className="whitespace-nowrap text-xs text-ink-tertiary">
{desc}
</div>
</div>
</button>
))}
@@ -0,0 +1,86 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { ChevronDownIcon } from "./Icons";
/**
* Desplegable personalizado (sustituto del `<select>` nativo).
*/
export function CustomSelect({
id,
value,
onChange,
options = [],
disabled = false,
emptyLabel = "Cualquiera",
className = "",
menuClassName = "",
}) {
const [open, setOpen] = useState(false);
const rootRef = useRef(null);
const allOptions = useMemo(
() => [{ value: "", label: emptyLabel }, ...options],
[options, emptyLabel],
);
const selected =
allOptions.find((opt) => opt.value === value) ?? allOptions[0];
useEffect(() => {
function handleClick(event) {
if (rootRef.current && !rootRef.current.contains(event.target)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
function handlePick(optionValue) {
onChange(optionValue);
setOpen(false);
}
return (
<div ref={rootRef} className={`relative ${className}`.trim()}>
<button
type="button"
id={id}
disabled={disabled}
aria-haspopup="listbox"
aria-expanded={open}
onClick={() => !disabled && setOpen((o) => !o)}
className="inline-flex w-full min-w-[7.5rem] items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2 text-[13px] font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-50"
>
<span className="truncate">{selected.label}</span>
<ChevronDownIcon
className={`ml-auto shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
/>
</button>
{open && !disabled && (
<div
role="listbox"
aria-labelledby={id}
className={`absolute left-0 top-[calc(100%+4px)] z-50 max-h-56 min-w-full overflow-auto rounded-xl border border-surface-border-strong bg-surface-primary py-1 shadow-lg ${menuClassName}`.trim()}
>
{allOptions.map((opt) => (
<button
key={opt.value || "__any__"}
type="button"
role="option"
aria-selected={opt.value === value}
onClick={() => handlePick(opt.value)}
className={`flex w-full px-3 py-2 text-left text-[13px] transition-colors hover:bg-surface-secondary ${
opt.value === value
? "bg-surface-secondary/80 font-medium text-ink-primary"
: "text-ink-secondary"
}`}
>
{opt.label}
</button>
))}
</div>
)}
</div>
);
}