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:
@@ -49,19 +49,20 @@ export function ExportDropdown({
|
||||
}
|
||||
|
||||
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
|
||||
id="dashboard-export-destination"
|
||||
value={exportDestination}
|
||||
onChange={onExportDestinationChange}
|
||||
includeZip
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDownload}
|
||||
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 ? (
|
||||
<Spinner size={15} />
|
||||
|
||||
@@ -44,7 +44,7 @@ export function ResearcherCard({ researcher, actions = null }) {
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -48,28 +48,32 @@ export function SwordProfileSelect({
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
<div className="relative">
|
||||
<span className="w-full whitespace-nowrap text-center text-ink-tertiary sm:w-auto sm:text-left">
|
||||
Destino:
|
||||
</span>
|
||||
<div className="relative w-full sm:w-auto sm:flex-none">
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
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"
|
||||
>
|
||||
<ExportProfileIcon profile={selected.value} size={20} />
|
||||
<span className="truncate">{selected.label}</span>
|
||||
<ChevronDownIcon className="ml-auto shrink-0" />
|
||||
<span className="inline-flex min-w-0 items-center gap-2">
|
||||
<ExportProfileIcon profile={selected.value} size={20} />
|
||||
<span className="truncate text-center">{selected.label}</span>
|
||||
</span>
|
||||
<ChevronDownIcon className="pointer-events-none absolute right-3 top-1/2 shrink-0 -translate-y-1/2" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
role="listbox"
|
||||
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) => (
|
||||
<button
|
||||
@@ -91,7 +95,7 @@ export function SwordProfileSelect({
|
||||
<div className="text-sm font-medium text-ink-primary">
|
||||
{label}
|
||||
</div>
|
||||
<div className="whitespace-nowrap text-xs text-ink-tertiary">
|
||||
<div className="text-xs text-ink-tertiary sm:whitespace-nowrap">
|
||||
{desc}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Spinner } from "../ui/Spinner";
|
||||
* Primary action button on the dashboard. Swaps icon + colour scheme
|
||||
* 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 isSuccess = status === "success";
|
||||
|
||||
@@ -20,7 +20,7 @@ export function SyncButton({ onClick, status = "idle" }) {
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
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 ? (
|
||||
<Spinner size={15} />
|
||||
|
||||
@@ -224,7 +224,11 @@ export function DashboardPage() {
|
||||
researcher={researcher}
|
||||
actions={
|
||||
<>
|
||||
<SyncButton onClick={handleSync} status={syncStatus} />
|
||||
<SyncButton
|
||||
onClick={handleSync}
|
||||
status={syncStatus}
|
||||
className="w-full sm:w-auto"
|
||||
/>
|
||||
<ExportDropdown
|
||||
onExport={handleExport}
|
||||
exportingFormat={exportingFormat}
|
||||
|
||||
@@ -14,8 +14,13 @@ import {
|
||||
UsersIcon,
|
||||
} from "../components/ui/Icons";
|
||||
import { downloadExport, searchResearchersBulk } from "../services/api";
|
||||
import { DEFAULT_EXPORT_PROFILE, swordXmlFilename } from "../utils/exportProfiles";
|
||||
import { SwordProfileSelect } from "../components/dashboard/SwordProfileSelect";
|
||||
import {
|
||||
DEFAULT_EXPORT_DESTINATION,
|
||||
DEFAULT_EXPORT_PROFILE,
|
||||
EXPORT_ZIP_DESTINATION,
|
||||
swordXmlFilename,
|
||||
} from "../utils/exportProfiles";
|
||||
import { ExportDropdown } from "../components/dashboard/ExportDropdown";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
|
||||
/**
|
||||
@@ -36,6 +41,9 @@ export function GroupResultsPage() {
|
||||
const [errors, setErrors] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [globalExporting, setGlobalExporting] = useState(null); // format | null
|
||||
const [globalExportDestination, setGlobalExportDestination] = useState(
|
||||
DEFAULT_EXPORT_DESTINATION,
|
||||
);
|
||||
const [swordProfile, setSwordProfile] = useState(DEFAULT_EXPORT_PROFILE);
|
||||
|
||||
// Track per-researcher export state (format | null)
|
||||
@@ -102,6 +110,14 @@ export function GroupResultsPage() {
|
||||
[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) {
|
||||
const ids = isAuthenticated ? allNewIds : allIds;
|
||||
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 (
|
||||
<div className="flex min-h-screen flex-col bg-surface-tertiary">
|
||||
<AppHeader variant="group" />
|
||||
@@ -233,33 +239,15 @@ export function GroupResultsPage() {
|
||||
|
||||
{/* Global export buttons */}
|
||||
{!loading && results.length > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<SwordProfileSelect
|
||||
id="group-sword-profile"
|
||||
value={swordProfile}
|
||||
onChange={setSwordProfile}
|
||||
/>
|
||||
{["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>
|
||||
<ExportDropdown
|
||||
onExport={handleGlobalExport}
|
||||
exportingFormat={globalExporting}
|
||||
selectedCount={0}
|
||||
isAuthenticated={isAuthenticated}
|
||||
newPublicationsCount={allNewIds.length}
|
||||
exportDestination={globalExportDestination}
|
||||
onExportDestinationChange={handleGlobalExportDestinationChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user