Task #692: Auto-scroll bookings day grid to current time (red now-line)

- BookingDayGrid (artifacts/tx-os/src/pages/protocol.tsx) now renders inside
  its own scroll container (max-h-[60vh] overflow-y-auto overscroll-contain).
- New scrollRef + useEffect: when the selected day is today and the now-line
  is within grid hours (showNow), sets scrollTop = nowTop - clientHeight/3
  so the red line sits ~1/3 from the top; otherwise resets scroll to top.
  Effect deps: [dayKey, showNow, scrollSignal] — not re-run on minute ticks,
  so it never fights user scrolling.
- "اليوم" button bumps a todayScrollSignal counter passed as optional
  scrollSignal prop, re-scrolling even when the selected day is already today.
- Removed the previous outer max-h-[70vh] wrapper around BookingDayGrid to
  avoid nested scrollbars.
- No changes to booking block layout, statuses, or list view. RTL-safe
  (logical properties untouched).
- tsc --noEmit clean for tx-os; architect review addressed (60vh per spec).
This commit is contained in:
Replit Agent
2026-07-08 12:43:22 +00:00
parent c8be955b64
commit 9184a0c223
+40 -11
View File
@@ -353,6 +353,7 @@ function BookingDayGrid({
isAr,
roomName,
emptyText,
scrollSignal = 0,
}: {
bookings: Booking[];
selectedDay: Date;
@@ -360,6 +361,7 @@ function BookingDayGrid({
isAr: boolean;
roomName: (id: number) => string;
emptyText: string;
scrollSignal?: number;
}) {
const hours: number[] = [];
for (let h = GRID_HOUR_START; h <= GRID_HOUR_END; h++) hours.push(h);
@@ -391,6 +393,25 @@ function BookingDayGrid({
hour12: true,
}).format(new Date(iso));
// The grid scrolls inside its own fixed-height container so we can jump
// straight to the red "now" line without touching the page scroll. When
// the selected day is today, position the line ~1/3 from the top of the
// viewport; for other days start at the top.
const scrollRef = useRef<HTMLDivElement | null>(null);
const dayKey = `${selectedDay.getFullYear()}-${selectedDay.getMonth()}-${selectedDay.getDate()}`;
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
if (showNow) {
el.scrollTop = Math.max(0, nowTop - el.clientHeight / 3);
} else {
el.scrollTop = 0;
}
// Re-run only when the day changes (or the now-line appears/disappears),
// not on every minute tick — so we never fight the user's scrolling.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dayKey, showNow, scrollSignal]);
return (
<div>
{dayItems.length === 0 && (
@@ -398,7 +419,11 @@ function BookingDayGrid({
{emptyText}
</div>
)}
<div className="flex" style={{ height: gridHeight + 8 }}>
<div
ref={scrollRef}
className="max-h-[60vh] overflow-y-auto overscroll-contain"
>
<div className="flex" style={{ height: gridHeight + 8 }}>
<div className="relative w-14 shrink-0" style={{ height: gridHeight }}>
{hours.map((h) => (
<div
@@ -474,6 +499,7 @@ function BookingDayGrid({
</div>
)}
</div>
</div>
</div>
</div>
);
@@ -628,6 +654,9 @@ export default function ProtocolPage() {
setSelectedBookings(new Set());
}, [debouncedBookingSearch, bookingsView, tab]);
const [selectedDay, setSelectedDay] = useState<Date>(() => new Date());
// Bumped by the "اليوم" button to re-scroll the day grid to the now-line
// even when the selected day is already today.
const [todayScrollSignal, setTodayScrollSignal] = useState(0);
const [calendarMonth, setCalendarMonth] = useState<Date>(() => new Date());
const now = useNow(60000);
const bookedDates = useMemo(() => {
@@ -1444,6 +1473,7 @@ export default function ProtocolPage() {
const today = new Date();
setSelectedDay(today);
setCalendarMonth(today);
setTodayScrollSignal((v) => v + 1);
}}
>
<CalendarDays size={14} className="me-1" />
@@ -1462,16 +1492,15 @@ export default function ProtocolPage() {
)}
</div>
</div>
<div className="max-h-[70vh] overflow-y-auto">
<BookingDayGrid
bookings={bookings.data ?? []}
selectedDay={selectedDay}
now={now}
isAr={isAr}
roomName={roomName}
emptyText={t("protocol.bookings.emptyDay")}
/>
</div>
<BookingDayGrid
bookings={bookings.data ?? []}
selectedDay={selectedDay}
now={now}
isAr={isAr}
roomName={roomName}
emptyText={t("protocol.bookings.emptyDay")}
scrollSignal={todayScrollSignal}
/>
</div>
</div>
)}