refactor(countdown): drop indirection left over from the row sections

renderRow had a single call site and recomputed activeEntryId, which the
caller had already derived to pick the scroll anchor. Inlining it brings
the row back to the shape it had before the sections were introduced.

The section wrapper no longer carries flex-grow. The list is sized by its
content and never fills the column, so there is no free space to
distribute and the growth factor resolved to nothing. Sections stay flex
containers though, otherwise the row margins collapse and the rows of a
section sit closer together than the sections themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjX7D56AW8cL3FXjvtnjtL
This commit is contained in:
Claude
2026-08-12 05:45:55 +00:00
parent 0106145a45
commit ce7717f26d
3 changed files with 48 additions and 51 deletions
@@ -18,11 +18,11 @@
padding-bottom: 95vh;
}
/* a group and its events, bounds the sticky group header to its own section */
/* a group and its events, bounds the sticky group header to its own section.
repeats the gap of the surrounding list, which no longer reaches the rows of a section */
.groupSection {
display: flex;
flex-direction: column;
/* the rows used to be direct children of the list, keep the gap they had */
gap: 2px;
}
@@ -103,12 +103,12 @@ $item-height: 3.5rem;
padding-bottom: max(8rem, calc(5rem + env(safe-area-inset-bottom)));
}
/* a subscribed group and its subscribed events, bounds the sticky header to its own section */
/* a subscribed group and its subscribed events, bounds the sticky header to its own section.
flex rather than block so that the row margins do not collapse, keeping the spacing between
rows of a section equal to the spacing between sections */
.sub-section {
display: flex;
flex-direction: column;
/* flex-grow is set inline from the row count, so that the rows keep the share they had in the list */
flex: 1 1 0;
}
/* scoped to the section, the select view shows the same cards in a flat list */
@@ -1,6 +1,6 @@
import { MaybeNumber, OntimeEvent } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IoPencil } from 'react-icons/io5';
import Button from '../../common/components/buttons/Button';
@@ -112,44 +112,6 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
throttledHandleScroll();
};
const renderRow = (event: CountdownTarget, rowRef: RefObject<HTMLDivElement | null> | undefined, isLive: boolean) => {
// while a group is live, surface the running event's title as the secondary line
const liveTitle = event.isGroup && event.liveEntry ? event.liveEntry.title : undefined;
const secondaryData = liveTitle ?? getPropertyValue(event, secondarySource);
const isGroupedEvent = !event.isGroup && Boolean(event.parent);
const activeEntryId = event.isGroup ? (event.liveEntry?.id ?? event.targetId) : event.id;
const isArmed = !isLive && activeEntryId === selectedEventId;
const countdownEvent = extendEventData(event, currentDay, actualStart, plannedStart, offset, mode, reportData);
const displayTitle = getPropertyValue(event, mainSource ?? 'title');
return (
<div
key={event.id}
ref={rowRef}
className={cx([
'sub',
isLive && 'sub--live',
isArmed && 'sub--armed',
event.isGroup && 'sub--group',
isGroupedEvent && 'sub--in-group',
])}
data-testid={event.cue}
>
<div
className='sub__binder'
style={{ '--user-color': event.colour, '--group-color': event.groupColour ?? 'transparent' }}
/>
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
<SubscriptionStatus event={countdownEvent} />
<div className={cx(['sub__title', !displayTitle && 'subdued'])}>
{event.isGroup && <span className='sub__eyebrow'>Group</span>}
{displayTitle}
</div>
{secondaryData && <div className='sub__secondary'>{secondaryData}</div>}
</div>
);
};
return (
<div className='list-container' onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
{sections.map((section) => {
@@ -158,21 +120,56 @@ export default function CountdownSubscriptions({ subscribedEvents, goToEditMode
const anchorId = section.events.find((event) => getIsLive(event.id, selectedEventId, playback))?.id ?? null;
return (
<div
key={section.group?.id ?? rows[0].id}
className='sub-section'
// the rows used to be direct children of the list, keep their share of the available space
style={{ flexGrow: rows.length }}
>
<div key={section.group?.id ?? rows[0].id} className='sub-section'>
{rows.map((event) => {
// while a group is live, surface the running event's title as the secondary line
const liveTitle = event.isGroup && event.liveEntry ? event.liveEntry.title : undefined;
const secondaryData = liveTitle ?? getPropertyValue(event, secondarySource);
const isGroupedEvent = !event.isGroup && Boolean(event.parent);
const activeEntryId = event.isGroup ? (event.liveEntry?.id ?? event.targetId) : event.id;
// a subscribed group is live when any of its children is the selected/running event
const isLive = activeEntryId ? getIsLive(activeEntryId, selectedEventId, playback) : false;
const isArmed = !isLive && activeEntryId === selectedEventId;
// only ever hand the ref to a single row, sharing it would null it out on the next commit
const isAnchor = isLive && (anchorId === null || event.id === anchorId);
const rowRef = isAnchor ? selectedRef : event.isGroup && anchorId ? stickyHeaderRef : undefined;
const countdownEvent = extendEventData(
event,
currentDay,
actualStart,
plannedStart,
offset,
mode,
reportData,
);
const displayTitle = getPropertyValue(event, mainSource ?? 'title');
return renderRow(event, rowRef, isLive);
return (
<div
key={event.id}
ref={rowRef}
className={cx([
'sub',
isLive && 'sub--live',
isArmed && 'sub--armed',
event.isGroup && 'sub--group',
isGroupedEvent && 'sub--in-group',
])}
data-testid={event.cue}
>
<div
className='sub__binder'
style={{ '--user-color': event.colour, '--group-color': event.groupColour ?? 'transparent' }}
/>
<ScheduleTime event={countdownEvent} showExpected={showExpected} />
<SubscriptionStatus event={countdownEvent} />
<div className={cx(['sub__title', !displayTitle && 'subdued'])}>
{event.isGroup && <span className='sub__eyebrow'>Group</span>}
{displayTitle}
</div>
{secondaryData && <div className='sub__secondary'>{secondaryData}</div>}
</div>
);
})}
</div>
);