ui: degradado de cabeceras y ajustes responsive

This commit is contained in:
marcos 2026-05-07 17:52:12 +02:00
parent f4c885031b
commit d209959d0e
15 changed files with 100 additions and 63 deletions

View File

@ -72,7 +72,17 @@
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%}
--sidebar-ring: 217.2 91.2% 59.8%;
--header-gradient: linear-gradient(
120deg,
hsl(220 95% 94% / 0.95) 0%,
hsl(232 95% 95% / 0.92) 22%,
hsl(246 95% 96% / 0.9) 48%,
hsl(260 95% 95% / 0.88) 72%,
hsl(214 95% 94% / 0.95) 100%
);
}
.dark {
--background: 0 0% 3.9%;
@ -138,7 +148,17 @@
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%}
--sidebar-ring: 217.2 91.2% 59.8%;
--header-gradient: linear-gradient(
120deg,
hsl(220 45% 12% / 0.9) 0%,
hsl(232 45% 14% / 0.88) 22%,
hsl(246 45% 15% / 0.86) 48%,
hsl(260 45% 14% / 0.86) 72%,
hsl(214 45% 12% / 0.9) 100%
);
}
}
@ -182,3 +202,11 @@
gap: 1rem;
}
}
@layer utilities {
.header-gradient {
background-image: var(--header-gradient);
background-size: 200% 200%;
background-position: 50% 0;
}
}

View File

@ -3,20 +3,23 @@ import { ProjectsTable } from "@/components/projects/projects-table";
export default function ProjectsPage() {
return (
<div className="flex flex-col gap-6 p-6">
{/* Header */}
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<FolderKanban className="h-6 w-6 text-primary" />
<h1 className="text-2xl font-bold tracking-tight">Proyectos</h1>
<div className="flex min-w-0 flex-col gap-6">
<header className="header-gradient border-b px-6 py-6">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<FolderKanban className="h-6 w-6 text-primary" />
<h1 className="text-2xl font-bold tracking-tight">Proyectos</h1>
</div>
<p className="text-muted-foreground">
Gestiona y visualiza todos tus proyectos en un solo lugar.
</p>
</div>
<p className="text-muted-foreground">
Gestiona y visualiza todos tus proyectos en un solo lugar.
</p>
</div>
</header>
{/* Data Table */}
<ProjectsTable />
<div className="p-6">
<ProjectsTable />
</div>
</div>
);
}

View File

@ -19,7 +19,7 @@ export default function DashboardHeader({
onCreateProject
}: DashboardHeaderProps) {
return (
<header className="bg-card border-b">
<header className="header-gradient border-b">
<div className="px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div>
@ -62,4 +62,4 @@ export default function DashboardHeader({
</div>
</header>
);
}
}

View File

@ -279,7 +279,7 @@ export default function GanttPage() {
return (
<div className="min-h-screen bg-background">
<section className="border-b bg-gradient-to-r from-blue-50/70 via-background to-violet-50/70 dark:from-blue-950/25 dark:via-background dark:to-violet-950/25">
<section className="border-b header-gradient">
<div className="px-4 sm:px-6 lg:px-8 py-5 space-y-4">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div className="flex items-center gap-3">

View File

@ -55,7 +55,11 @@ export function MainLayout({ children }: MainLayoutProps) {
<AppSidebar />
<SidebarInset>
<main>
<SidebarTrigger />
<div className="header-gradient border-b">
<div className="flex items-center px-4 py-2">
<SidebarTrigger />
</div>
</div>
{children}
</main>
</SidebarInset>

View File

@ -185,13 +185,13 @@ export function ProjectDetailView({ project: initialProject }: ProjectDetailView
/>
{/* Contenido principal */}
<div className="p-6 space-y-6">
<div className="min-w-0 p-6 space-y-6">
{/* Stats cards */}
<ProjectStats project={project} taskStats={taskStats} />
{/* Tabs de contenido */}
<Tabs defaultValue="overview" className="space-y-6">
<TabsList>
<TabsList className="flex flex-wrap items-center gap-1">
<TabsTrigger value="overview" className="gap-2">
<LayoutDashboard className="h-4 w-4" />
Resumen

View File

@ -72,7 +72,7 @@ export function ProjectHeader({ project, tasks = [], onProjectUpdated, onProject
};
return (
<div className="border-b bg-card">
<div className="border-b header-gradient">
<div className="px-6 py-4">
{/* Navegación */}
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-4">
@ -84,8 +84,8 @@ export function ProjectHeader({ project, tasks = [], onProjectUpdated, onProject
</div>
{/* Header principal */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex min-w-0 items-start gap-4">
{/* Avatar */}
<Avatar className="h-16 w-16 shrink-0">
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white font-semibold text-xl">
@ -94,13 +94,15 @@ export function ProjectHeader({ project, tasks = [], onProjectUpdated, onProject
</Avatar>
{/* Info principal */}
<div className="space-y-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold tracking-tight">{project.name}</h1>
<div className="min-w-0 space-y-1">
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-2xl font-bold tracking-tight break-words">
{project.name}
</h1>
<ProjectStatusBadge status={project.status} className="text-sm px-3 py-1" />
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
<button
onClick={handleCopyRef}
className="flex items-center gap-1.5 hover:text-foreground transition-colors font-mono"
@ -109,19 +111,16 @@ export function ProjectHeader({ project, tasks = [], onProjectUpdated, onProject
{project.ref}
<Copy className="h-3.5 w-3.5" />
</button>
{project.client !== "Sin cliente" && (
<>
<span className="text-muted-foreground/50">|</span>
<span>{project.client}</span>
</>
<span>{project.client}</span>
)}
</div>
</div>
</div>
{/* Acciones */}
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" onClick={handleExportPDF}>
<Download className="h-4 w-4 mr-2" />
Exportar PDF

View File

@ -195,8 +195,9 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
const maxTimelineWidth = Math.max(320, fitTimelineWidth);
const baseTickWidth = zoom === "fit" ? 72 : zoom === "week" ? 84 : 36;
const tickWidth = Math.min(baseTickWidth, maxTimelineWidth / Math.max(1, ticks.length));
const timelineWidth = Math.max(320, ticks.length * tickWidth);
const rawTimelineWidth = ticks.length * baseTickWidth;
const timelineWidth = Math.max(320, Math.min(maxTimelineWidth, rawTimelineWidth));
const tickWidth = timelineWidth / Math.max(1, ticks.length);
const today = startOfDay(new Date());
const showToday = today >= visibleStart && today <= visibleEnd;

View File

@ -254,7 +254,7 @@ export function TasksSection({ tasks, projectId, isLoading, onTaskCreated, onTas
}
return (
<div className="space-y-4">
<div className="min-w-0 space-y-4">
{/* Stats rápidos */}
<div className="flex flex-wrap items-center gap-4 text-sm">
<div className="flex items-center gap-2">
@ -282,10 +282,10 @@ export function TasksSection({ tasks, projectId, isLoading, onTaskCreated, onTas
</div>
{/* Toolbar */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 items-center gap-2">
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-1 flex-wrap items-center gap-2">
{/* Búsqueda */}
<div className="relative w-full sm:w-[250px]">
<div className="relative w-full sm:w-[240px]">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar tareas..."
@ -297,7 +297,7 @@ export function TasksSection({ tasks, projectId, isLoading, onTaskCreated, onTas
{/* Filtro estado */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[130px] h-9">
<SelectTrigger className="w-full sm:w-[130px] h-9">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
@ -310,7 +310,7 @@ export function TasksSection({ tasks, projectId, isLoading, onTaskCreated, onTas
{/* Filtro prioridad */}
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-[130px] h-9">
<SelectTrigger className="w-full sm:w-[130px] h-9">
<SelectValue placeholder="Prioridad" />
</SelectTrigger>
<SelectContent>
@ -331,7 +331,7 @@ export function TasksSection({ tasks, projectId, isLoading, onTaskCreated, onTas
)}
</div>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
{/* Toggle vista */}
<div className="flex items-center border rounded-md">
<Button

View File

@ -76,10 +76,10 @@ export function DataTableToolbar({
};
return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 items-center gap-2">
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-1 flex-wrap items-center gap-2">
{/* Búsqueda */}
<div className="relative w-full sm:w-[300px]">
<div className="relative w-full sm:w-[260px]">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar proyectos..."
@ -91,7 +91,7 @@ export function DataTableToolbar({
{/* Filtro por estado */}
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger className="w-[150px]">
<SelectTrigger className="w-full sm:w-[150px]">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
@ -119,7 +119,7 @@ export function DataTableToolbar({
)}
</div>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
{/* Contador de seleccionados */}
{selectedCount > 0 && (
<Badge variant="secondary" className="rounded-sm px-2 font-normal">
@ -136,7 +136,7 @@ export function DataTableToolbar({
Vista
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[180px]">
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuLabel>Columnas visibles</DropdownMenuLabel>
<DropdownMenuSeparator />
{columns
@ -158,9 +158,9 @@ export function DataTableToolbar({
)}
{/* Exportar */}
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
className="h-9"
onClick={handleExport}
disabled={!onExport}

View File

@ -120,9 +120,9 @@ export function DataTable<TData, TValue>({
}, [searchKey, searchValue, table]);
return (
<div className="space-y-4">
<div className="min-w-0 space-y-4">
{/* Tabla */}
<div className="rounded-md border">
<div className="min-w-0 rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (

View File

@ -180,7 +180,7 @@ export function ProjectsTable() {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<DataTableToolbar
searchValue={searchValue}
onSearchChange={setSearchValue}
@ -193,7 +193,7 @@ export function ProjectsTable() {
getSelectedProjects={tableInfo?.getSelectedRows}
getAllVisibleProjects={tableInfo?.getAllVisibleRows}
/>
<Button onClick={() => setIsCreateSheetOpen(true)} size="sm">
<Button onClick={() => setIsCreateSheetOpen(true)} size="sm" className="shrink-0">
<Plus className="h-4 w-4 mr-2" />
Nuevo proyecto
</Button>

View File

@ -1058,7 +1058,7 @@ export default function StatisticsPage() {
return (
<div className="min-h-screen bg-background">
<header className="bg-card border-b sticky top-0 z-10">
<header className="header-gradient border-b sticky top-0 z-10">
<div className="px-6 py-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg">

View File

@ -89,7 +89,7 @@ export function TaskHeader({ task, project, onTaskUpdated, onTaskDeleted }: Task
};
return (
<div className="border-b bg-card">
<div className="border-b header-gradient">
<div className="px-6 py-4">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm text-muted-foreground mb-4">

View File

@ -40,15 +40,17 @@ const AUTH_USER_KEY = "dolibarr_auth_user";
* Obtiene la URL base de la API de Dolibarr
*/
function getApiUrl(): string {
// Intentar obtener del entorno (cliente usa NEXT_PUBLIC_)
const url = typeof window !== "undefined"
? process.env.NEXT_PUBLIC_API_URL
: process.env.DOLIBARR_API_URL || process.env.NEXT_PUBLIC_API_URL;
// En cliente, usar proxy para evitar problemas de red/CORS
if (typeof window !== "undefined") {
return "/api/dolibarr";
}
const url = process.env.DOLIBARR_API_URL || process.env.NEXT_PUBLIC_API_URL;
if (!url) {
throw new Error("API URL not configured");
}
return url;
}