refactor(layout): remove site-header and tighten spacing
replace site-header with inline sidebar trigger, reduce dashboard padding/gaps, and adjust schedule page layout to flex properly within the content area.
This commit is contained in:
parent
598047635d
commit
e6c1b7c4a0
@ -1,8 +1,8 @@
|
|||||||
import { AppSidebar } from "@/components/app-sidebar"
|
import { AppSidebar } from "@/components/app-sidebar"
|
||||||
import { SiteHeader } from "@/components/site-header"
|
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
|
|
||||||
export default function DashboardLayout({
|
export default function DashboardLayout({
|
||||||
@ -15,15 +15,16 @@ export default function DashboardLayout({
|
|||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": "calc(var(--spacing) * 72)",
|
"--sidebar-width": "calc(var(--spacing) * 72)",
|
||||||
"--header-height": "calc(var(--spacing) * 12)",
|
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppSidebar variant="inset" />
|
<AppSidebar variant="inset" />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<SiteHeader />
|
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="@container/main flex flex-1 flex-col gap-2">
|
<div className="@container/main flex flex-1 flex-col">
|
||||||
|
<div className="px-4 pt-2">
|
||||||
|
<SidebarTrigger className="-ml-1" />
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import data from "./data.json"
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
<div className="flex flex-col gap-3 py-2 md:gap-4 md:py-3">
|
||||||
<SectionCards />
|
<SectionCards />
|
||||||
<div className="px-4 lg:px-6">
|
<div className="px-3 lg:px-4">
|
||||||
<ChartAreaInteractive />
|
<ChartAreaInteractive />
|
||||||
</div>
|
</div>
|
||||||
<DataTable data={data} />
|
<DataTable data={data} />
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export default async function SchedulePage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="px-4 py-2 flex flex-col flex-1 min-h-0">
|
||||||
<ScheduleView
|
<ScheduleView
|
||||||
projectId={id}
|
projectId={id}
|
||||||
projectName={projectName}
|
projectName={projectName}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useRef, useEffect, useState } from "react"
|
import { useRef, useEffect, useState } from "react"
|
||||||
import type { FrappeTask } from "@/lib/schedule/gantt-transform"
|
import type { FrappeTask } from "@/lib/schedule/gantt-transform"
|
||||||
|
import "./gantt.css"
|
||||||
|
|
||||||
type ViewMode = "Day" | "Week" | "Month"
|
type ViewMode = "Day" | "Week" | "Month"
|
||||||
|
|
||||||
@ -66,6 +67,15 @@ export function GanttChart({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// remove overflow from inner gantt-container so popup isn't clipped
|
||||||
|
// the parent wrapper handles horizontal scrolling instead
|
||||||
|
const ganttContainer = containerRef.current.querySelector(
|
||||||
|
".gantt-container"
|
||||||
|
) as HTMLElement | null
|
||||||
|
if (ganttContainer) {
|
||||||
|
ganttContainer.style.overflow = "visible"
|
||||||
|
}
|
||||||
|
|
||||||
setLoaded(true)
|
setLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +98,7 @@ export function GanttChart({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gantt-container overflow-x-auto">
|
<div className="gantt-wrapper relative overflow-x-auto">
|
||||||
<div ref={containerRef} />
|
<div ref={containerRef} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* frappe-gantt base styles (vendored from frappe-gantt/dist/frappe-gantt.css) */
|
/* frappe-gantt base styles (vendored from frappe-gantt/dist/frappe-gantt.css) */
|
||||||
:root{--g-arrow-color: #1f2937;--g-bar-color: #fff;--g-bar-border: #fff;--g-tick-color-thick: #ededed;--g-tick-color: #f3f3f3;--g-actions-background: #f3f3f3;--g-border-color: #ebeff2;--g-text-muted: #7c7c7c;--g-text-light: #fff;--g-text-dark: #171717;--g-progress-color: #dbdbdb;--g-handle-color: #37352f;--g-weekend-label-color: #dcdce4;--g-expected-progress: #c4c4e9;--g-header-background: #fff;--g-row-color: #fdfdfd;--g-row-border-color: #c7c7c7;--g-today-highlight: #37352f;--g-popup-actions: #ebeff2;--g-weekend-highlight-color: #f7f7f7}
|
:root{--g-arrow-color: #1f2937;--g-bar-color: #fff;--g-bar-border: #fff;--g-tick-color-thick: #ededed;--g-tick-color: #f3f3f3;--g-actions-background: #f3f3f3;--g-border-color: #ebeff2;--g-text-muted: #7c7c7c;--g-text-light: #fff;--g-text-dark: #171717;--g-progress-color: #dbdbdb;--g-handle-color: #37352f;--g-weekend-label-color: #dcdce4;--g-expected-progress: #c4c4e9;--g-header-background: #fff;--g-row-color: #fdfdfd;--g-row-border-color: #c7c7c7;--g-today-highlight: #37352f;--g-popup-actions: #ebeff2;--g-weekend-highlight-color: #f7f7f7}
|
||||||
.gantt-container{line-height:14.5px;position:relative;overflow:auto;font-size:12px;height:var(--gv-grid-height);width:100%;border-radius:8px}
|
.gantt-container{line-height:14.5px;position:relative;isolation:isolate;overflow:auto;font-size:12px;height:var(--gv-grid-height);width:100%;border-radius:8px}
|
||||||
.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:#fff;box-shadow:0 10px 24px -3px #0003;padding:10px;border-radius:5px;width:max-content;z-index:1000}
|
.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:#fff;box-shadow:0 10px 24px -3px #0003;padding:10px;border-radius:5px;width:max-content;z-index:1000}
|
||||||
.gantt-container .popup-wrapper .title{margin-bottom:2px;color:var(--g-text-dark);font-size:.85rem;font-weight:650;line-height:15px}
|
.gantt-container .popup-wrapper .title{margin-bottom:2px;color:var(--g-text-dark);font-size:.85rem;font-weight:650;line-height:15px}
|
||||||
.gantt-container .popup-wrapper .subtitle{color:var(--g-text-dark);font-size:.8rem;margin-bottom:5px}
|
.gantt-container .popup-wrapper .subtitle{color:var(--g-text-dark);font-size:.8rem;margin-bottom:5px}
|
||||||
|
|||||||
@ -103,8 +103,8 @@ export function ScheduleCalendarView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -144,7 +144,7 @@ export function ScheduleCalendarView({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border rounded-md overflow-hidden">
|
<div className="border rounded-md overflow-hidden flex flex-col flex-1 min-h-0">
|
||||||
<div className="grid grid-cols-7 border-b">
|
<div className="grid grid-cols-7 border-b">
|
||||||
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map(
|
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map(
|
||||||
(day) => (
|
(day) => (
|
||||||
@ -158,7 +158,7 @@ export function ScheduleCalendarView({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7">
|
<div className="grid grid-cols-7 flex-1">
|
||||||
{days.map((day) => {
|
{days.map((day) => {
|
||||||
const dateKey = format(day, "yyyy-MM-dd")
|
const dateKey = format(day, "yyyy-MM-dd")
|
||||||
const dayTasks = tasksByDate.get(dateKey) || []
|
const dayTasks = tasksByDate.get(dateKey) || []
|
||||||
@ -174,7 +174,7 @@ export function ScheduleCalendarView({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={dateKey}
|
key={dateKey}
|
||||||
className={`min-h-[90px] border-r border-b last:border-r-0 p-1 ${
|
className={`min-h-0 border-r border-b last:border-r-0 p-1 ${
|
||||||
!inMonth ? "bg-muted/30" : ""
|
!inMonth ? "bg-muted/30" : ""
|
||||||
} ${isNonWork ? "bg-muted/50" : ""}`}
|
} ${isNonWork ? "bg-muted/50" : ""}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -87,8 +87,8 @@ export function ScheduleGanttView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{(["Day", "Week", "Month"] as ViewMode[]).map((mode) => (
|
{(["Day", "Week", "Month"] as ViewMode[]).map((mode) => (
|
||||||
<Button
|
<Button
|
||||||
@ -134,7 +134,7 @@ export function ScheduleGanttView({
|
|||||||
|
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
className="border rounded-md min-h-[400px]"
|
className="border rounded-md flex-1 min-h-[300px]"
|
||||||
>
|
>
|
||||||
<ResizablePanel defaultSize={30} minSize={20}>
|
<ResizablePanel defaultSize={30} minSize={20}>
|
||||||
<div className="h-full overflow-auto">
|
<div className="h-full overflow-auto">
|
||||||
|
|||||||
@ -296,8 +296,8 @@ export function ScheduleListView({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -309,7 +309,7 @@ export function ScheduleListView({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border flex-1 overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export function ScheduleToolbar({ onNewItem }: ScheduleToolbarProps) {
|
|||||||
const [offlineMode, setOfflineMode] = useState(false)
|
const [offlineMode, setOfflineMode] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between py-3 border-b mb-4">
|
<div className="flex items-center justify-between py-1.5 border-b mb-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="ghost" size="icon" className="size-8">
|
<Button variant="ghost" size="icon" className="size-8">
|
||||||
<IconSettings className="size-4" />
|
<IconSettings className="size-4" />
|
||||||
|
|||||||
@ -35,31 +35,37 @@ export function ScheduleView({
|
|||||||
const [taskFormOpen, setTaskFormOpen] = useState(false)
|
const [taskFormOpen, setTaskFormOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h1 className="text-2xl font-semibold">
|
<h1 className="text-lg font-semibold">
|
||||||
{projectName} - Schedule
|
{projectName} - Schedule
|
||||||
</h1>
|
</h1>
|
||||||
|
<Tabs
|
||||||
|
value={topTab}
|
||||||
|
onValueChange={(v) => setTopTab(v as TopTab)}
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="schedule">Schedule</TabsTrigger>
|
||||||
|
<TabsTrigger value="baseline">Baseline</TabsTrigger>
|
||||||
|
<TabsTrigger value="exceptions">
|
||||||
|
Workday Exceptions
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={topTab}
|
value={topTab}
|
||||||
onValueChange={(v) => setTopTab(v as TopTab)}
|
onValueChange={(v) => setTopTab(v as TopTab)}
|
||||||
|
className="flex flex-col flex-1 min-h-0"
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsContent value="schedule" className="mt-0 flex flex-col flex-1 min-h-0">
|
||||||
<TabsTrigger value="schedule">Schedule</TabsTrigger>
|
|
||||||
<TabsTrigger value="baseline">Baseline</TabsTrigger>
|
|
||||||
<TabsTrigger value="exceptions">
|
|
||||||
Workday Exceptions
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="schedule" className="mt-0">
|
|
||||||
<ScheduleToolbar onNewItem={() => setTaskFormOpen(true)} />
|
<ScheduleToolbar onNewItem={() => setTaskFormOpen(true)} />
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={subTab}
|
value={subTab}
|
||||||
onValueChange={(v) => setSubTab(v as ScheduleSubTab)}
|
onValueChange={(v) => setSubTab(v as ScheduleSubTab)}
|
||||||
|
className="flex flex-col flex-1 min-h-0"
|
||||||
>
|
>
|
||||||
<TabsList className="bg-transparent border-b rounded-none h-auto p-0 gap-4">
|
<TabsList className="bg-transparent border-b rounded-none h-auto p-0 gap-4">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
@ -82,7 +88,7 @@ export function ScheduleView({
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="calendar" className="mt-4">
|
<TabsContent value="calendar" className="mt-2 flex flex-col flex-1 min-h-0">
|
||||||
<ScheduleCalendarView
|
<ScheduleCalendarView
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
tasks={initialData.tasks}
|
tasks={initialData.tasks}
|
||||||
@ -90,7 +96,7 @@ export function ScheduleView({
|
|||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="list" className="mt-4">
|
<TabsContent value="list" className="mt-2 flex flex-col flex-1 min-h-0">
|
||||||
<ScheduleListView
|
<ScheduleListView
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
tasks={initialData.tasks}
|
tasks={initialData.tasks}
|
||||||
@ -98,7 +104,7 @@ export function ScheduleView({
|
|||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="gantt" className="mt-4">
|
<TabsContent value="gantt" className="mt-2 flex flex-col flex-1 min-h-0">
|
||||||
<ScheduleGanttView
|
<ScheduleGanttView
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
tasks={initialData.tasks}
|
tasks={initialData.tasks}
|
||||||
@ -108,7 +114,7 @@ export function ScheduleView({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="baseline" className="mt-4">
|
<TabsContent value="baseline" className="mt-2">
|
||||||
<ScheduleBaselineView
|
<ScheduleBaselineView
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
baselines={baselines}
|
baselines={baselines}
|
||||||
@ -116,7 +122,7 @@ export function ScheduleView({
|
|||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="exceptions" className="mt-4">
|
<TabsContent value="exceptions" className="mt-2">
|
||||||
<WorkdayExceptionsView
|
<WorkdayExceptionsView
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
exceptions={initialData.exceptions}
|
exceptions={initialData.exceptions}
|
||||||
|
|||||||
@ -19,6 +19,14 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Calendar } from "@/components/ui/calendar"
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
|
import { IconCalendar } from "@tabler/icons-react"
|
||||||
|
import { format, parseISO } from "date-fns"
|
||||||
import { Slider } from "@/components/ui/slider"
|
import { Slider } from "@/components/ui/slider"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@ -178,9 +186,33 @@ export function TaskFormDialog({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Start Date</FormLabel>
|
<FormLabel>Start Date</FormLabel>
|
||||||
<FormControl>
|
<Popover>
|
||||||
<Input type="date" {...field} />
|
<PopoverTrigger asChild>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left font-normal"
|
||||||
|
>
|
||||||
|
<IconCalendar className="size-4 mr-2 text-muted-foreground" />
|
||||||
|
{field.value
|
||||||
|
? format(parseISO(field.value), "MMM d, yyyy")
|
||||||
|
: "Pick a date"}
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={field.value ? parseISO(field.value) : undefined}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (date) {
|
||||||
|
field.onChange(format(date, "yyyy-MM-dd"))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
export function SectionCards() {
|
export function SectionCards() {
|
||||||
return (
|
return (
|
||||||
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-3 px-3 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-4 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
|
||||||
<Card className="@container/card">
|
<Card className="@container/card">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardDescription>Total Revenue</CardDescription>
|
<CardDescription>Total Revenue</CardDescription>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user