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-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 { .dark {
--background: 0 0% 3.9%; --background: 0 0% 3.9%;
@ -138,7 +148,17 @@
--sidebar-border: 240 3.7% 15.9%; --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; 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() { export default function ProjectsPage() {
return ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex min-w-0 flex-col gap-6">
{/* Header */} <header className="header-gradient border-b px-6 py-6">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FolderKanban className="h-6 w-6 text-primary" /> <FolderKanban className="h-6 w-6 text-primary" />
<h1 className="text-2xl font-bold tracking-tight">Proyectos</h1> <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> </div>
<p className="text-muted-foreground"> </header>
Gestiona y visualiza todos tus proyectos en un solo lugar.
</p>
</div>
{/* Data Table */} {/* Data Table */}
<ProjectsTable /> <div className="p-6">
<ProjectsTable />
</div>
</div> </div>
); );
} }

View File

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

View File

@ -279,7 +279,7 @@ export default function GanttPage() {
return ( return (
<div className="min-h-screen bg-background"> <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="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-start justify-between gap-4 flex-wrap">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -180,7 +180,7 @@ export function ProjectsTable() {
return ( return (
<div className="space-y-4"> <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 <DataTableToolbar
searchValue={searchValue} searchValue={searchValue}
onSearchChange={setSearchValue} onSearchChange={setSearchValue}
@ -193,7 +193,7 @@ export function ProjectsTable() {
getSelectedProjects={tableInfo?.getSelectedRows} getSelectedProjects={tableInfo?.getSelectedRows}
getAllVisibleProjects={tableInfo?.getAllVisibleRows} 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" /> <Plus className="h-4 w-4 mr-2" />
Nuevo proyecto Nuevo proyecto
</Button> </Button>

View File

@ -1058,7 +1058,7 @@ export default function StatisticsPage() {
return ( return (
<div className="min-h-screen bg-background"> <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="px-6 py-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg"> <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 ( return (
<div className="border-b bg-card"> <div className="border-b header-gradient">
<div className="px-6 py-4"> <div className="px-6 py-4">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm text-muted-foreground mb-4"> <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 * Obtiene la URL base de la API de Dolibarr
*/ */
function getApiUrl(): string { function getApiUrl(): string {
// Intentar obtener del entorno (cliente usa NEXT_PUBLIC_) // En cliente, usar proxy para evitar problemas de red/CORS
const url = typeof window !== "undefined" if (typeof window !== "undefined") {
? process.env.NEXT_PUBLIC_API_URL return "/api/dolibarr";
: process.env.DOLIBARR_API_URL || process.env.NEXT_PUBLIC_API_URL; }
const url = process.env.DOLIBARR_API_URL || process.env.NEXT_PUBLIC_API_URL;
if (!url) { if (!url) {
throw new Error("API URL not configured"); throw new Error("API URL not configured");
} }
return url; return url;
} }