mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
fixed alignment errors when scrolling to the side
This commit is contained in:
@@ -21,7 +21,8 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
const TIMELINE_LABEL_WIDTH = "240px";
|
const TIMELINE_LABEL_WIDTH = 240;
|
||||||
|
const WEEK_COLUMN_WIDTH = 140;
|
||||||
|
|
||||||
const addDays = (value: Date, days: number) =>
|
const addDays = (value: Date, days: number) =>
|
||||||
new Date(value.getFullYear(), value.getMonth(), value.getDate() + days);
|
new Date(value.getFullYear(), value.getMonth(), value.getDate() + days);
|
||||||
@@ -56,7 +57,7 @@ type IssueGroup = {
|
|||||||
type TimelineRange = {
|
type TimelineRange = {
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
durationMs: number;
|
totalDays: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Timeline() {
|
export default function Timeline() {
|
||||||
@@ -133,9 +134,9 @@ export default function Timeline() {
|
|||||||
|
|
||||||
const rangeStart = today;
|
const rangeStart = today;
|
||||||
const rangeEnd = addDays(today, 60);
|
const rangeEnd = addDays(today, 60);
|
||||||
const durationMs = rangeEnd.getTime() - rangeStart.getTime() + DAY_MS;
|
const totalDays = Math.round((rangeEnd.getTime() - rangeStart.getTime()) / DAY_MS);
|
||||||
|
|
||||||
return { start: rangeStart, end: rangeEnd, durationMs };
|
return { start: rangeStart, end: rangeEnd, totalDays };
|
||||||
}, [sprints]);
|
}, [sprints]);
|
||||||
|
|
||||||
const weeks = useMemo(() => {
|
const weeks = useMemo(() => {
|
||||||
@@ -168,28 +169,32 @@ export default function Timeline() {
|
|||||||
const statuses = selectedOrganisation?.Organisation.statuses ?? {};
|
const statuses = selectedOrganisation?.Organisation.statuses ?? {};
|
||||||
|
|
||||||
const gridTemplateColumns = useMemo(() => {
|
const gridTemplateColumns = useMemo(() => {
|
||||||
if (weeks.length === 0) return `${TIMELINE_LABEL_WIDTH} 1fr`;
|
if (weeks.length === 0) return `${TIMELINE_LABEL_WIDTH}px 1fr`;
|
||||||
return `${TIMELINE_LABEL_WIDTH} repeat(${weeks.length}, minmax(140px, 1fr))`;
|
return `${TIMELINE_LABEL_WIDTH}px repeat(${weeks.length}, ${WEEK_COLUMN_WIDTH}px)`;
|
||||||
}, [weeks.length]);
|
}, [weeks.length]);
|
||||||
|
|
||||||
const todayMarker = useMemo(() => {
|
const todayMarker = useMemo(() => {
|
||||||
if (!timelineRange) return null;
|
if (!timelineRange) return null;
|
||||||
const today = toDate(new Date());
|
const today = toDate(new Date());
|
||||||
if (today < timelineRange.start || today > timelineRange.end) return null;
|
if (today < timelineRange.start || today > timelineRange.end) return null;
|
||||||
const left = ((today.getTime() - timelineRange.start.getTime()) / timelineRange.durationMs) * 100;
|
const dayOffset = (today.getTime() - timelineRange.start.getTime()) / DAY_MS;
|
||||||
return { left: `${left}%`, label: formatTodayLabel(today) };
|
const left = dayOffset * (WEEK_COLUMN_WIDTH / 7);
|
||||||
|
return { left: `${left}px`, label: formatTodayLabel(today) };
|
||||||
}, [timelineRange]);
|
}, [timelineRange]);
|
||||||
|
|
||||||
const getSprintBarStyle = (sprint: SprintRecord) => {
|
const getSprintBarStyle = (sprint: SprintRecord) => {
|
||||||
if (!timelineRange) return null;
|
if (!timelineRange) return null;
|
||||||
const start = toDate(sprint.startDate);
|
const start = toDate(sprint.startDate);
|
||||||
const end = addDays(toDate(sprint.endDate), 1);
|
const end = addDays(toDate(sprint.endDate), 1);
|
||||||
const left = ((start.getTime() - timelineRange.start.getTime()) / timelineRange.durationMs) * 100;
|
const dayWidth = WEEK_COLUMN_WIDTH / 7;
|
||||||
const right = ((end.getTime() - timelineRange.start.getTime()) / timelineRange.durationMs) * 100;
|
const startOffset = (start.getTime() - timelineRange.start.getTime()) / DAY_MS;
|
||||||
const width = Math.max(right - left, 1);
|
const endOffset = (end.getTime() - timelineRange.start.getTime()) / DAY_MS;
|
||||||
|
const visibleStart = Math.max(0, startOffset);
|
||||||
|
const visibleEnd = Math.min(timelineRange.totalDays, endOffset);
|
||||||
|
const width = Math.max(visibleEnd - visibleStart, 0.25) * dayWidth;
|
||||||
return {
|
return {
|
||||||
left: `${left}%`,
|
left: `${visibleStart * dayWidth}px`,
|
||||||
width: `${width}%`,
|
width: `${width}px`,
|
||||||
backgroundColor: sprint.color || DEFAULT_SPRINT_COLOUR,
|
backgroundColor: sprint.color || DEFAULT_SPRINT_COLOUR,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -220,7 +225,11 @@ export default function Timeline() {
|
|||||||
{selectedOrganisationId && selectedProjectId && sprints.length > 0 && (
|
{selectedOrganisationId && selectedProjectId && sprints.length > 0 && (
|
||||||
<div className="border">
|
<div className="border">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="min-w-[720px]">
|
<div
|
||||||
|
style={{
|
||||||
|
minWidth: `${TIMELINE_LABEL_WIDTH + weeks.length * WEEK_COLUMN_WIDTH}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="grid border-b bg-muted/20" style={{ gridTemplateColumns }}>
|
<div className="grid border-b bg-muted/20" style={{ gridTemplateColumns }}>
|
||||||
<div
|
<div
|
||||||
className={`px-${BREATHING_ROOM} py-${BREATHING_ROOM} text-xs font-medium text-muted-foreground bg-background border-r`}
|
className={`px-${BREATHING_ROOM} py-${BREATHING_ROOM} text-xs font-medium text-muted-foreground bg-background border-r`}
|
||||||
@@ -278,14 +287,19 @@ export default function Timeline() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(`py-${BREATHING_ROOM} relative min-h-12`, "border-l")}
|
className={cn(`py-${BREATHING_ROOM} relative min-h-12`)}
|
||||||
style={{ gridColumn: "2 / -1" }}
|
style={{ gridColumn: "2 / -1" }}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 flex z-10 pointer-events-none">
|
<div
|
||||||
|
className="absolute inset-0 grid z-20 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${weeks.length}, ${WEEK_COLUMN_WIDTH}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{weeks.map((week, index) => (
|
{weeks.map((week, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${week.toISOString()}-${sprint.id}`}
|
key={`${week.toISOString()}-${sprint.id}`}
|
||||||
className={cn("flex-1", index === 0 ? "" : "border-l")}
|
className={cn(index === 0 ? "" : "border-l")}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -313,7 +327,7 @@ export default function Timeline() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Edit sprint ${sprint.name}`}
|
aria-label={`Edit sprint ${sprint.name}`}
|
||||||
className="absolute top-1/2 z-0 h-4 rounded border border-foreground/10 cursor-pointer"
|
className="absolute top-1/2 z-0 h-4 rounded cursor-pointer"
|
||||||
style={barStyle}
|
style={barStyle}
|
||||||
title={`${sprint.name}: ${getSprintDateRange(sprint)}`}
|
title={`${sprint.name}: ${getSprintDateRange(sprint)}`}
|
||||||
/>
|
/>
|
||||||
@@ -329,7 +343,7 @@ export default function Timeline() {
|
|||||||
<div
|
<div
|
||||||
className={`px-${BREATHING_ROOM} pt-0.5 py-${BREATHING_ROOM} flex flex-col gap-${BREATHING_ROOM} bg-background relative z-20 border-r`}
|
className={`px-${BREATHING_ROOM} pt-0.5 py-${BREATHING_ROOM} flex flex-col gap-${BREATHING_ROOM} bg-background relative z-20 border-r`}
|
||||||
>
|
>
|
||||||
<div className="text-sm font-medium text-muted-foreground">Backlog</div>
|
<div className="text-sm font-medium">Backlog</div>
|
||||||
{issueGroup.unassigned.length === 0 && (
|
{issueGroup.unassigned.length === 0 && (
|
||||||
<div className="text-xs text-muted-foreground text-pretty">No unassigned issues.</div>
|
<div className="text-xs text-muted-foreground text-pretty">No unassigned issues.</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user