feat: editor layout

This commit is contained in:
Carlos Valente
2026-01-21 19:56:52 +01:00
committed by Carlos Valente
parent 967e92e2c8
commit da02ecd113
77 changed files with 2267 additions and 788 deletions
@@ -2,7 +2,13 @@ import { memo, PropsWithChildren } from 'react';
import { useIsMobileScreen } from '../../common/hooks/useIsMobileScreen';
import { ClockOverview, MetadataTimes, OffsetOverview, StartTimes, TimerOverview } from './composite/TimeElements';
import {
ClockOverview,
MetadataTimes,
OffsetOverview,
StartTimesRuntime,
TimerOverview,
} from './composite/TimeElements';
import TitleOverview from './composite/TitleOverview';
import { OverviewWrapper } from './OverviewWrapper';
@@ -10,8 +16,10 @@ export default memo(CuesheetOverview);
function CuesheetOverview({ children }: PropsWithChildren) {
const isMobileScreen = useIsMobileScreen();
if (isMobileScreen) return <CuesheetMobile>{children}</CuesheetMobile>;
else return <CuesheetDesktop>{children}</CuesheetDesktop>;
if (isMobileScreen) {
return <CuesheetMobile>{children}</CuesheetMobile>;
}
return <CuesheetDesktop>{children}</CuesheetDesktop>;
}
function CuesheetMobile({ children }: PropsWithChildren) {
@@ -27,7 +35,7 @@ function CuesheetDesktop({ children }: PropsWithChildren) {
return (
<OverviewWrapper navElements={children}>
<TitleOverview />
<StartTimes shouldFormat />
<StartTimesRuntime shouldFormat />
<TimerOverview />
<OffsetOverview />
<MetadataTimes />
@@ -1,19 +1,73 @@
import { memo, PropsWithChildren } from 'react';
import { ClockOverview, MetadataTimes, OffsetOverview, ProgressOverview, StartTimes } from './composite/TimeElements';
import { EditorLayoutMode, useEditorLayout } from '../../views/editor/useEditorLayout';
import {
ClockOverview,
MetadataTimes,
OffsetOverview,
PlanningStats,
ProgressOverview,
StartTimesPlanning,
StartTimesRuntime,
} from './composite/TimeElements';
import TitleOverview from './composite/TitleOverview';
import { OverviewWrapper } from './OverviewWrapper';
import style from './Overview.module.scss';
export default memo(EditorOverview);
function EditorOverview({ children }: PropsWithChildren) {
const { layoutMode } = useEditorLayout();
return (
<OverviewWrapper navElements={children}>
<TitleOverview />
<StartTimes />
<ProgressOverview />
<OffsetOverview />
<MetadataTimes />
<ClockOverview />
{layoutMode === EditorLayoutMode.PLANNING && <OverviewPlanning />}
{layoutMode === EditorLayoutMode.TRACKING && <OverviewTracking />}
{layoutMode === EditorLayoutMode.CONTROL && <OverviewControl />}
</OverviewWrapper>
);
}
function OverviewPlanning() {
return (
<>
<div className={style.inline}>
<TitleOverview />
<StartTimesPlanning />
<PlanningStats />
</div>
<ClockOverview />
</>
);
}
function OverviewTracking() {
return (
<>
<div className={style.inline}>
<StartTimesRuntime />
<ProgressOverview />
<OffsetOverview />
</div>
<MetadataTimes />
<ClockOverview />
</>
);
}
function OverviewControl() {
return (
<>
<TitleOverview />
<div className={style.inline}>
<StartTimesRuntime />
<ProgressOverview />
<OffsetOverview />
</div>
<MetadataTimes />
<ClockOverview />
</>
);
}
@@ -29,11 +29,22 @@
}
.info {
flex: 1;
padding-left: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
overflow-x: auto;
overflow-y: hidden;
width: max-content;
height: 100%;
}
.infoScroll {
flex: 1;
min-width: 0;
height: 100%;
}
.inline {
display: flex;
align-items: center;
gap: 1rem;
}
@@ -1,6 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react';
import { ErrorBoundary } from '@sentry/react';
import ScrollArea from '../../common/components/scroll-area/ScrollArea';
import { useIsOnline } from '../../common/hooks/useSocket';
import { cx } from '../../common/utils/styleUtils';
@@ -17,7 +18,14 @@ export function OverviewWrapper({ navElements, children }: PropsWithChildren<Ove
<div className={cx([style.overview, !isOnline && style.isOffline])}>
<ErrorBoundary>
<div className={style.nav}>{navElements}</div>
<div className={style.info}>{children}</div>
<ScrollArea
className={style.infoScroll}
contentClassName={style.info}
contentStyle={{ minWidth: '100%' }}
orientation='horizontal'
>
{children}
</ScrollArea>
</ErrorBoundary>
</div>
);
@@ -14,6 +14,13 @@
gap: 0.5rem;
}
.row2 {
display: grid;
grid-template-columns: 3rem 9rem;
align-items: center;
gap: 0.5rem;
}
.metadataRow {
display: grid;
grid-template-columns: minmax(3rem, 10rem) 8rem 8rem;
@@ -27,7 +27,7 @@ import {
import { useEntry } from '../../../common/hooks-query/useRundown';
import { getOffsetState, getOffsetText } from '../../../common/utils/offset';
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
import { formatTime } from '../../../common/utils/time';
import { formatDuration, formatTime } from '../../../common/utils/time';
import SuperscriptPeriod from '../../../views/common/superscript-time/SuperscriptPeriod';
import { calculateEndAndDaySpan, formatDueTime } from '../overview.utils';
@@ -39,30 +39,22 @@ interface OverviewTimeElementsProps {
shouldFormat?: boolean;
}
export function StartTimes({ shouldFormat }: OverviewTimeElementsProps) {
const timeFormatOptions = { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' };
function formatTimeValue(time: number | null, shouldFormat: boolean | undefined): string {
if (time === null) return timerPlaceholder;
if (shouldFormat) return formatTime(time, timeFormatOptions);
return millisToString(time, { fallback: timerPlaceholder });
}
export function StartTimesRuntime({ shouldFormat }: OverviewTimeElementsProps) {
const { plannedEnd, plannedStart, actualStart } = useStartTimesOverview();
const formatOptions = { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' };
const plannedStartText = (() => {
if (plannedStart === null) return timerPlaceholder;
if (shouldFormat) return formatTime(plannedStart, formatOptions);
return millisToString(plannedStart, { fallback: timerPlaceholder });
})();
const actualStartText = (() => {
if (actualStart === null) return timerPlaceholder;
if (shouldFormat) return formatTime(actualStart, formatOptions);
return millisToString(actualStart, { fallback: timerPlaceholder });
})();
const plannedStartText = formatTimeValue(plannedStart, shouldFormat);
const actualStartText = formatTimeValue(actualStart, shouldFormat);
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
const plannedEndText = (() => {
if (maybePlannedEnd === null) return timerPlaceholder;
if (shouldFormat) return formatTime(maybePlannedEnd, formatOptions);
return millisToString(maybePlannedEnd, { fallback: timerPlaceholder });
})();
const plannedEndText = formatTimeValue(maybePlannedEnd, shouldFormat);
const multipleDays = maybePlannedDaySpan > 0;
const plannedEndTooltip = multipleDays
@@ -122,6 +114,59 @@ export function StartTimes({ shouldFormat }: OverviewTimeElementsProps) {
);
}
export function StartTimesPlanning({ shouldFormat }: OverviewTimeElementsProps) {
const { plannedEnd, plannedStart } = useStartTimesOverview();
const plannedStartText = formatTimeValue(plannedStart, shouldFormat);
const [maybePlannedEnd, maybePlannedDaySpan] = useMemo(() => calculateEndAndDaySpan(plannedEnd), [plannedEnd]);
const plannedEndText = formatTimeValue(maybePlannedEnd, shouldFormat);
const multipleDays = maybePlannedDaySpan > 0;
const plannedEndTooltip = multipleDays
? `Planned end time (rundown spans over ${maybePlannedDaySpan + 1} days)`
: 'Planned end time';
return (
<div className={style.column}>
<div className={style.row2}>
<span className={style.label}>Start</span>
<Tooltip
text='Planned start time'
render={
<div className={style.labelledElement}>
<TbCalendarPin className={style.icon} />
<SuperscriptPeriod
className={cx([style.time, plannedStart === null && style.muted])}
time={plannedStartText}
/>
</div>
}
/>
</div>
<div className={style.row2}>
<span className={style.label}>End</span>
<Tooltip
text={plannedEndTooltip}
render={
<div className={style.labelledElement}>
<TbCalendarPin className={style.icon} />
<SuperscriptPeriod
className={cx([style.time, plannedEnd === null && style.muted])}
time={plannedEndText}
/>
{multipleDays && (
<span className={cx([style.time, style.daySpan])} data-day-offset={maybePlannedDaySpan} />
)}
</div>
}
/>
</div>
</div>
);
}
/**
* Shows the expected end for the rundown
* Extracted to improve performance as this is a ticking value
@@ -130,11 +175,7 @@ function RundownExpectedEnd({ shouldFormat }: OverviewTimeElementsProps) {
const expectedEnd = useRundownExpectedEnd();
const [maybeExpectedEnd, maybeExpectedDaySpan] = useMemo(() => calculateEndAndDaySpan(expectedEnd), [expectedEnd]);
const maybeExpectedEndText = (() => {
if (maybeExpectedEnd === null) return timerPlaceholder;
if (shouldFormat) return formatTime(maybeExpectedEnd, { format12: 'hh:mm:ss a', format24: 'HH:mm:ss' });
return millisToString(maybeExpectedEnd, { fallback: timerPlaceholder });
})();
const maybeExpectedEndText = formatTimeValue(maybeExpectedEnd, shouldFormat);
const multipleDays = maybeExpectedEnd !== null && maybeExpectedDaySpan > 0;
const tooltip = multipleDays
@@ -316,11 +357,27 @@ export function TimerOverview({ className }: { className?: string }) {
const isWaiting = timer.phase === TimerPhase.Pending;
const title = isWaiting ? 'Count to start' : 'Running timer';
const display = millisToString(isWaiting ? timer.secondaryTimer : timer.current, { fallback: timerPlaceholder });
const timerState = (() => {
function getTimerState(): 'waiting' | 'muted' | 'active' {
if (isWaiting) return 'waiting';
if (timer.current === null) return 'muted';
return 'active';
})();
}
return <TimeColumn label={title} value={display} state={timerState} className={className} />;
return <TimeColumn label={title} value={display} state={getTimerState()} className={className} />;
}
export function PlanningStats() {
const { numEvents } = useProgressOverview();
const { plannedEnd, plannedStart } = useStartTimesOverview();
const hasTimes = plannedStart !== null && plannedEnd !== null;
const formattedDuration = hasTimes ? formatDuration(plannedEnd - plannedStart) : timerPlaceholder;
return (
<>
<TimeColumn label='Total duration' value={formattedDuration} />
<TimeColumn label='Events' value={String(numEvents)} />
</>
);
}