feat(ui): mejorar estilos y estructura de componentes en el dashboard

Se ajustan los estilos y la estructura de varios componentes en el dashboard, incluyendo ExportDropdown, ResearcherCard, SwordProfileSelect y SyncButton, para mejorar la presentación y la responsividad. Se añade soporte para clases personalizadas en SyncButton y se integra el nuevo sistema de exportación en GroupResultsPage.
This commit is contained in:
Alexis
2026-06-02 10:44:51 +02:00
parent ddab663d50
commit 6603ddfe23
6 changed files with 51 additions and 54 deletions
@@ -49,19 +49,20 @@ export function ExportDropdown({
} }
return ( return (
<div className="flex flex-wrap items-center justify-end gap-2 sm:flex-nowrap"> <div className="mx-auto flex w-full max-w-[440px] flex-col items-stretch gap-2 sm:mx-0 sm:w-auto sm:max-w-none sm:flex-row sm:items-center sm:justify-end">
<SwordProfileSelect <SwordProfileSelect
id="dashboard-export-destination" id="dashboard-export-destination"
value={exportDestination} value={exportDestination}
onChange={onExportDestinationChange} onChange={onExportDestinationChange}
includeZip includeZip
className="w-full"
/> />
<button <button
type="button" type="button"
onClick={handleDownload} onClick={handleDownload}
disabled={isBusy || nothingToDownload} disabled={isBusy || nothingToDownload}
className="inline-flex items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-[18px] py-2.5 text-sm font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-70" className="inline-flex w-full items-center justify-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-[18px] py-2.5 text-sm font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-70 sm:w-auto"
> >
{isBusy ? ( {isBusy ? (
<Spinner size={15} /> <Spinner size={15} />
@@ -44,7 +44,7 @@ export function ResearcherCard({ researcher, actions = null }) {
</div> </div>
{actions && ( {actions && (
<div className="ml-auto flex shrink-0 flex-col items-end gap-2.5"> <div className="flex w-full flex-col items-stretch gap-2.5 md:ml-auto md:w-auto md:shrink-0 md:items-end">
{actions} {actions}
</div> </div>
)} )}
@@ -48,28 +48,32 @@ export function SwordProfileSelect({
return ( return (
<div <div
ref={rootRef} ref={rootRef}
className={`flex items-center gap-2 text-sm ${className}`.trim()} className={`flex w-full flex-col items-start gap-1.5 text-sm sm:w-auto sm:flex-row sm:items-center sm:gap-2 ${className}`.trim()}
> >
<span className="whitespace-nowrap text-ink-tertiary">Destino:</span> <span className="w-full whitespace-nowrap text-center text-ink-tertiary sm:w-auto sm:text-left">
<div className="relative"> Destino:
</span>
<div className="relative w-full sm:w-auto sm:flex-none">
<button <button
type="button" type="button"
id={id} id={id}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded={open} aria-expanded={open}
onClick={() => setOpen((o) => !o)} onClick={() => setOpen((o) => !o)}
className="inline-flex min-w-44 items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2.5 text-sm font-medium text-ink-primary transition-colors hover:bg-surface-secondary" className="relative inline-flex w-full min-w-44 items-center justify-center rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2.5 pr-10 text-sm font-medium text-ink-primary transition-colors hover:bg-surface-secondary sm:w-auto"
> >
<span className="inline-flex min-w-0 items-center gap-2">
<ExportProfileIcon profile={selected.value} size={20} /> <ExportProfileIcon profile={selected.value} size={20} />
<span className="truncate">{selected.label}</span> <span className="truncate text-center">{selected.label}</span>
<ChevronDownIcon className="ml-auto shrink-0" /> </span>
<ChevronDownIcon className="pointer-events-none absolute right-3 top-1/2 shrink-0 -translate-y-1/2" />
</button> </button>
{open && ( {open && (
<div <div
role="listbox" role="listbox"
aria-labelledby={id} aria-labelledby={id}
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" className="absolute left-0 top-[calc(100%+6px)] z-50 w-full min-w-0 overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg sm:min-w-80"
> >
{options.map(({ value: optionValue, label, desc }, idx) => ( {options.map(({ value: optionValue, label, desc }, idx) => (
<button <button
@@ -91,7 +95,7 @@ export function SwordProfileSelect({
<div className="text-sm font-medium text-ink-primary"> <div className="text-sm font-medium text-ink-primary">
{label} {label}
</div> </div>
<div className="whitespace-nowrap text-xs text-ink-tertiary"> <div className="text-xs text-ink-tertiary sm:whitespace-nowrap">
{desc} {desc}
</div> </div>
</div> </div>
@@ -5,7 +5,7 @@ import { Spinner } from "../ui/Spinner";
* Primary action button on the dashboard. Swaps icon + colour scheme * Primary action button on the dashboard. Swaps icon + colour scheme
* depending on the sync lifecycle (idle → loading → success flash). * depending on the sync lifecycle (idle → loading → success flash).
*/ */
export function SyncButton({ onClick, status = "idle" }) { export function SyncButton({ onClick, status = "idle", className = "" }) {
const isLoading = status === "loading"; const isLoading = status === "loading";
const isSuccess = status === "success"; const isSuccess = status === "success";
@@ -20,7 +20,7 @@ export function SyncButton({ onClick, status = "idle" }) {
type="button" type="button"
onClick={onClick} onClick={onClick}
disabled={isLoading} disabled={isLoading}
className={`inline-flex items-center gap-2 rounded-lg px-[18px] py-2.5 text-sm font-medium transition-colors disabled:cursor-not-allowed ${palette}`} className={`inline-flex items-center justify-center gap-2 rounded-lg px-[18px] py-2.5 text-sm font-medium transition-colors disabled:cursor-not-allowed ${palette} ${className}`.trim()}
> >
{isLoading ? ( {isLoading ? (
<Spinner size={15} /> <Spinner size={15} />
+5 -1
View File
@@ -224,7 +224,11 @@ export function DashboardPage() {
researcher={researcher} researcher={researcher}
actions={ actions={
<> <>
<SyncButton onClick={handleSync} status={syncStatus} /> <SyncButton
onClick={handleSync}
status={syncStatus}
className="w-full sm:w-auto"
/>
<ExportDropdown <ExportDropdown
onExport={handleExport} onExport={handleExport}
exportingFormat={exportingFormat} exportingFormat={exportingFormat}
+26 -38
View File
@@ -14,8 +14,13 @@ import {
UsersIcon, UsersIcon,
} from "../components/ui/Icons"; } from "../components/ui/Icons";
import { downloadExport, searchResearchersBulk } from "../services/api"; import { downloadExport, searchResearchersBulk } from "../services/api";
import { DEFAULT_EXPORT_PROFILE, swordXmlFilename } from "../utils/exportProfiles"; import {
import { SwordProfileSelect } from "../components/dashboard/SwordProfileSelect"; DEFAULT_EXPORT_DESTINATION,
DEFAULT_EXPORT_PROFILE,
EXPORT_ZIP_DESTINATION,
swordXmlFilename,
} from "../utils/exportProfiles";
import { ExportDropdown } from "../components/dashboard/ExportDropdown";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
/** /**
@@ -36,6 +41,9 @@ export function GroupResultsPage() {
const [errors, setErrors] = useState([]); const [errors, setErrors] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [globalExporting, setGlobalExporting] = useState(null); // format | null const [globalExporting, setGlobalExporting] = useState(null); // format | null
const [globalExportDestination, setGlobalExportDestination] = useState(
DEFAULT_EXPORT_DESTINATION,
);
const [swordProfile, setSwordProfile] = useState(DEFAULT_EXPORT_PROFILE); const [swordProfile, setSwordProfile] = useState(DEFAULT_EXPORT_PROFILE);
// Track per-researcher export state (format | null) // Track per-researcher export state (format | null)
@@ -102,6 +110,14 @@ export function GroupResultsPage() {
[results], [results],
); );
function handleGlobalExportDestinationChange(nextDestination) {
setGlobalExportDestination(nextDestination);
// Keep last XML profile for card-level exports.
if (nextDestination !== EXPORT_ZIP_DESTINATION) {
setSwordProfile(nextDestination);
}
}
async function handleGlobalExport(format, profile = DEFAULT_EXPORT_PROFILE) { async function handleGlobalExport(format, profile = DEFAULT_EXPORT_PROFILE) {
const ids = isAuthenticated ? allNewIds : allIds; const ids = isAuthenticated ? allNewIds : allIds;
if (ids.length === 0) { if (ids.length === 0) {
@@ -193,16 +209,6 @@ export function GroupResultsPage() {
} }
} }
const globalLabel = isAuthenticated
? allNewIds.length > 0
? `Descargar lo nuevo de todos (${allNewIds.length})`
: "Todo descargado"
: `Descargar todo (${allIds.length})`;
const globalDisabled =
Boolean(globalExporting) ||
(isAuthenticated ? allNewIds.length === 0 : allIds.length === 0);
return ( return (
<div className="flex min-h-screen flex-col bg-surface-tertiary"> <div className="flex min-h-screen flex-col bg-surface-tertiary">
<AppHeader variant="group" /> <AppHeader variant="group" />
@@ -233,33 +239,15 @@ export function GroupResultsPage() {
{/* Global export buttons */} {/* Global export buttons */}
{!loading && results.length > 0 && ( {!loading && results.length > 0 && (
<div className="flex flex-wrap items-center justify-end gap-2"> <ExportDropdown
<SwordProfileSelect onExport={handleGlobalExport}
id="group-sword-profile" exportingFormat={globalExporting}
value={swordProfile} selectedCount={0}
onChange={setSwordProfile} isAuthenticated={isAuthenticated}
newPublicationsCount={allNewIds.length}
exportDestination={globalExportDestination}
onExportDestinationChange={handleGlobalExportDestinationChange}
/> />
{["xml", "zip"].map((fmt) => (
<button
key={fmt}
type="button"
onClick={() => handleGlobalExport(fmt, swordProfile)}
disabled={globalDisabled}
className="inline-flex items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-4 py-2 text-sm font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-60"
>
{globalExporting === fmt ? (
<Spinner size={14} />
) : isAuthenticated && allNewIds.length > 0 ? (
<SparkleIcon size={13} className="text-brand-accent" />
) : (
<DownloadIcon size={14} />
)}
{globalExporting === fmt
? `Exportando ${fmt.toUpperCase()}...`
: `${fmt.toUpperCase()} · ${globalLabel}`}
</button>
))}
</div>
)} )}
</div> </div>