refactor: small style tweaks

refactor: consistent font size on inputs

refactor: style tweaks for block spacing

refactor: improve skipped styles

fix: improve responsiveness in extracted rundown

refactor: style tweaks to overview

refactor: simplify group target duration
This commit is contained in:
Carlos Valente
2025-08-02 06:49:51 +02:00
committed by Carlos Valente
parent 524721002a
commit a358ec0f17
20 changed files with 175 additions and 131 deletions
@@ -9,7 +9,7 @@ import style from './TimeInput.module.scss';
interface NullableTimeInputProps<T extends string> {
id?: T;
name: T;
submitHandler: (field: T, value: string) => void;
submitHandler: (field: T, value: number) => void;
time?: number | null;
emptyDisplay: string;
placeholder?: string;
@@ -71,7 +71,7 @@ export default function NullableTimeInput<T extends string>({
return false;
}
submitHandler(name, newValue);
submitHandler(name, valueInMillis);
return true;
},
[name, submitHandler, time],
@@ -14,7 +14,6 @@
border: 1px solid transparent;
padding-inline: 0.5rem;
font-size: calc(1rem - 2px);
white-space: nowrap;
&:hover:not([data-disabled]) {
@@ -57,7 +56,7 @@
}
.popup {
font-size: calc(1rem - 2px);
font-size: 1rem;
background-color: $gray-1200;
box-sizing: border-box;
padding: 2px;
+2 -2
View File
@@ -19,7 +19,7 @@ export function getOffsetText(offset: MaybeNumber): string {
}
export function getOffsetState(offset: MaybeNumber): 'over' | 'under' | 'muted' | null {
if (offset === null) return null;
if (offset === 0) return 'muted';
if (offset === null) return 'muted';
if (offset === 0) return null;
return offset < 0 ? 'over' : 'under';
}
@@ -202,7 +202,7 @@ export function OffsetOverview() {
const isPlaying = isPlaybackActive(playback);
const correctedOffset = offset * -1;
const offsetState = getOffsetState(offset);
const offsetState = getOffsetState(isPlaying ? offset : null);
const offsetText = getOffsetText(isPlaying ? correctedOffset : null);
return <OverUnder state={offsetState} value={offsetText} testId='offset' />;
@@ -9,14 +9,11 @@
letter-spacing: 0.5px;
min-width: 5em;
font-variant-numeric: tabular-nums;
color: $ui-white;
&::after {
content: '\200b';
}
&.muted {
color: $muted-gray;
}
}
.column {
@@ -32,7 +29,7 @@
gap: 0.25rem;
}
&[data-state="over"] {
&[data-state='over'] {
.label .over {
color: $playback-over;
}
@@ -41,7 +38,7 @@
}
}
&[data-state="under"] {
&[data-state='under'] {
.label .under {
color: $playback-under;
}
@@ -50,7 +47,7 @@
}
}
&[data-state="muted"] {
&[data-state='muted'] {
.clock {
color: $muted-gray;
}
@@ -13,9 +13,9 @@ interface TimeLayoutProps {
export function TimeColumn({ label, value, muted, className, testId }: TimeLayoutProps) {
return (
<div className={style.column}>
<div className={style.column} data-state={muted ? 'muted' : 'active'}>
<span className={style.label}>{label}</span>
<span className={cx([style.clock, muted && style.muted, className])} data-testid={testId}>
<span className={cx([style.clock, className])} data-testid={testId}>
{value}
</span>
</div>
+17 -14
View File
@@ -42,8 +42,8 @@ import { AppMode, sessionKeys } from '../../ontimeConfig';
import QuickAddButtons from './entry-editor/quick-add-buttons/QuickAddButtons';
import QuickAddInline from './entry-editor/quick-add-cursor/QuickAddInline';
import BlockEnd from './rundown-block/BlockEnd';
import RundownBlock from './rundown-block/RundownBlock';
import RundownBlockEnd from './rundown-block/RundownBlockEnd';
import { canDrop, makeRundownMetadata, makeSortableList } from './rundown.utils';
import RundownEmpty from './RundownEmpty';
import { useEventSelection } from './useEventSelection';
@@ -444,20 +444,23 @@ export default function Rundown({ data }: RundownProps) {
if (isBlockCollapsed) {
return null;
} else {
const parentColour = (entries[parentId] as OntimeBlock | undefined)?.colour;
// if the previous element is selected, it will have its own QuickAddInline
// we use thisId instead of previousEntryId because the block end does not process
// and it does not cause the reassignment of the iteration id to the previous entry
return (
<Fragment key={entryId}>
{isEditMode && rundownMetadata.groupEntries === 0 && (
<QuickAddButtons previousEventId={null} parentBlock={parentId} backgroundColor={parentColour} />
)}
<BlockEnd key={entryId} id={entryId} colour={parentColour} />
</Fragment>
);
}
// if the previous element is selected, it will have its own QuickAddInline
// we use thisId instead of previousEntryId because the block end does not process
// and it does not cause the reassignment of the iteration id to the previous entry
return (
<Fragment key={entryId}>
{isEditMode && rundownMetadata.groupEntries === 0 && (
<QuickAddButtons
previousEventId={null}
parentBlock={parentId}
backgroundColor={rundownMetadata.groupColour}
/>
)}
<RundownBlockEnd key={entryId} id={entryId} colour={rundownMetadata.groupColour} />
</Fragment>
);
}
// we iterate through a stateful copy of order to make the dnd operations smoother
@@ -37,7 +37,7 @@ function RundownExport() {
>
<FinderPlacement />
<ViewNavigationMenu suppressSettings />
<div className={style.content}>
<div className={style.rundown}>
<ErrorBoundary>
<RundownContextMenu>
<RundownWrapper isSmallDevice />
@@ -1,25 +1,25 @@
import { useCallback } from 'react';
import { OntimeBlock } from 'ontime-types';
import { millisToString, parseUserTime } from 'ontime-utils';
import { MaybeNumber, OntimeBlock } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
import SwatchSelect from '../../../common/components/input/colour-input/SwatchSelect';
import NullableTimeInput from '../../../common/components/input/time-input/NullableTimeInput';
import AppLink from '../../../common/components/link/app-link/AppLink';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import useCustomFields from '../../../common/hooks-query/useCustomFields';
import { getOffsetState } from '../../../common/utils/offset';
import { enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
import { cx, enDash, timerPlaceholder } from '../../../common/utils/styleUtils';
import TextLikeInput from '../../../views/cuesheet/cuesheet-table/cuesheet-table-elements/TextLikeInput';
import EntryEditorCustomFields from './composite/EventEditorCustomFields';
import EventTextArea from './composite/EventTextArea';
import EntryEditorTextInput from './composite/EventTextInput';
import TargetDurationInput from './composite/TargetDurationInput';
import style from './EntryEditor.module.scss';
// title + colour + custom field labels
export type BlockEditorUpdateTextFields = 'targetDuration' | 'title' | 'colour' | string;
export type BlockEditorUpdateTextFields = 'title' | 'colour' | string;
export type BlockEditorUpdateMaybeNumberFields = 'targetDuration';
interface BlockEditorProps {
@@ -31,7 +31,7 @@ export default function BlockEditor({ block }: BlockEditorProps) {
const { updateEntry } = useEntryActions();
const handleSubmit = useCallback(
(field: BlockEditorUpdateTextFields | BlockEditorUpdateMaybeNumberFields, value: string | boolean) => {
(field: BlockEditorUpdateTextFields | BlockEditorUpdateMaybeNumberFields, value: string | MaybeNumber) => {
// Handle custom fields
if (typeof field === 'string' && field.startsWith('custom-')) {
const fieldLabel = field.split('custom-')[1];
@@ -40,11 +40,7 @@ export default function BlockEditor({ block }: BlockEditorProps) {
}
if (field === 'targetDuration') {
if (value === '') {
return updateEntry({ id: block.id, targetDuration: null });
}
return updateEntry({ id: block.id, targetDuration: parseUserTime(value as string) });
return updateEntry({ id: block.id, targetDuration: value as MaybeNumber });
}
// all other strings are text fields
@@ -85,22 +81,22 @@ export default function BlockEditor({ block }: BlockEditorProps) {
</div>
</div>
<div className={style.inline}>
<div>
<Editor.Label htmlFor='targetDuration'>Target duration</Editor.Label>
<NullableTimeInput
name='targetDuration'
time={block.targetDuration}
submitHandler={handleSubmit}
emptyDisplay={enDash}
/>
</div>
<div>
<Editor.Label htmlFor='eventId'>Plan offset</Editor.Label>
<TextLikeInput offset={planOffsetLabel} className={style.textLikeInput} tabIndex={-1}>
<TextLikeInput
offset={planOffsetLabel}
className={cx([style.textLikeInput, planOffset === null && style.inactive])}
tabIndex={-1}
>
{planOffset !== null && planOffset > 0 ? '+' : ''}
{millisToString(planOffset, { fallback: enDash })}
</TextLikeInput>
</div>
<TargetDurationInput
duration={block.duration}
targetDuration={block.targetDuration}
submitHandler={handleSubmit}
/>
</div>
</div>
@@ -94,16 +94,21 @@
/* approximating the style of a disabled input */
.textLikeInput {
background-color: rgba($gray-1200, 0.4);
font-weight: 400;
color: $gray-200;
border: 1px solid transparent;
justify-content: center;
background: transparent;
justify-content: start;
width: 7.5em;
border: 1px solid transparent;
border-bottom: 1px solid $gray-1100;
&:hover {
background-color: rgba($gray-1200, 0.4);
background: transparent;
}
}
.inactive {
color: $muted-gray;
}
.active {
color: $active-indicator;
}
@@ -0,0 +1,45 @@
import { IoLockClosed, IoLockOpenOutline } from 'react-icons/io5';
import { MaybeNumber } from 'ontime-types';
import IconButton from '../../../../common/components/buttons/IconButton';
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
import NullableTimeInput from '../../../../common/components/input/time-input/NullableTimeInput';
import Tooltip from '../../../../common/components/tooltip/Tooltip';
import { cx, enDash } from '../../../../common/utils/styleUtils';
import TimeInputGroup from '../../time-input-flow/TimeInputGroup';
import style from '../EntryEditor.module.scss';
interface TargetDurationInputProps {
duration: MaybeNumber;
targetDuration: MaybeNumber;
submitHandler: (field: 'targetDuration', value: MaybeNumber) => void;
}
export default function TargetDurationInput({ duration, targetDuration, submitHandler }: TargetDurationInputProps) {
const isBlocked = targetDuration !== null;
return (
<div>
<Editor.Label htmlFor='targetDuration'>Target duration</Editor.Label>
<TimeInputGroup hasDelay={isBlocked && targetDuration !== duration}>
<NullableTimeInput
name='targetDuration'
time={targetDuration}
submitHandler={submitHandler}
emptyDisplay={enDash}
className={isBlocked ? '' : style.inactive}
/>
<Tooltip
text='Lock to target duration'
className={cx([style.timeAction, isBlocked && style.active])}
onClick={() => submitHandler('targetDuration', isBlocked ? null : duration)}
data-testid='lock__duration'
render={<IconButton variant='subtle-white' className={isBlocked ? style.active : style.inactive} />}
>
{isBlocked ? <IoLockClosed /> : <IoLockOpenOutline />}
</Tooltip>
</TimeInputGroup>
</div>
);
}
@@ -3,7 +3,7 @@
.block {
@include block-styling;
margin-block: 1rem 0.25rem;
margin-block: 0.5rem;
display: grid;
grid-template-columns: 2rem 1fr;
grid-template-areas: 'binder header';
@@ -14,7 +14,7 @@
}
&.expanded {
margin-block: 1rem 0;
margin-block: 0.5rem 0;
border-radius: $block-border-radius $block-border-radius 0 0;
border-bottom: 0.25rem solid color-mix(in srgb, transparent 90%, var(--user-bg, transparent) 10%);
}
@@ -6,5 +6,5 @@
background-color: var(--user-bg, $gray-1050);
border-radius: 0 0 $block-border-radius $block-border-radius;
margin-bottom: 0.25rem;
margin-bottom: 0.5rem;
}
@@ -1,14 +1,14 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import style from './BlockEnd.module.scss';
import style from './RundownBlockEnd.module.scss';
interface BlockEndProps {
id: string;
colour?: string;
}
export default function BlockEnd({ id, colour }: BlockEndProps) {
export default function RundownBlockEnd({ id, colour }: BlockEndProps) {
const {
attributes: dragAttributes,
listeners: dragListeners,
@@ -37,10 +37,10 @@ function RundownHeader() {
return (
<Toolbar.Root className={style.header}>
<ToggleGroup value={[editorMode]} onValueChange={toggleAppMode} className={style.group}>
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.button}>
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
Run
</Toolbar.Button>
<Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.button}>
<Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.radioButton}>
Edit
</Toolbar.Button>
</ToggleGroup>
@@ -48,10 +48,10 @@ function RundownHeader() {
<Editor.Separator className={style.separator} />
<ToggleGroup value={[offsetMode]} onValueChange={toggleOffsetMode} className={style.group}>
<Toolbar.Button render={<Toggle />} value={OffsetMode.Absolute} className={style.button}>
<Toolbar.Button render={<Toggle />} value={OffsetMode.Absolute} className={style.radioButton}>
Absolute
</Toolbar.Button>
<Toolbar.Button render={<Toggle />} value={OffsetMode.Relative} className={style.button}>
<Toolbar.Button render={<Toggle />} value={OffsetMode.Relative} className={style.radioButton}>
Relative
</Toolbar.Button>
</ToggleGroup>
@@ -1,31 +1,3 @@
.timeAction {
background: $gray-1050;
color: $gray-500;
cursor: pointer;
height: 2rem;
width: 2rem;
display: grid;
place-content: center;
aspect-ratio: 1;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
}
&:active:not(:disabled) {
background: $gray-1300;
}
&:focus {
outline: 1px solid $blue-500;
outline-offset: -1px;
}
&.active {
color: var(--status-color-active-override, $active-indicator);
}
}
.fourtyfive {
transform: rotate(-45deg);
}
@@ -36,28 +8,10 @@
font-size: 1.5em;
}
.inputGroup {
border: 1px solid transparent;
width: fit-content;
display: flex;
align-items: center;
border-radius: $component-border-radius-md;
&.delayed {
border: 1px solid $ontime-delay-text;
}
input {
max-width: 6.5em;
border-radius: $component-border-radius-md 0 0 $component-border-radius-md;
border: none;
padding-right: 0;
}
button {
width: 1.5rem;
height: 2rem;
border-radius: 0 $component-border-radius-md $component-border-radius-md 0;
border: none;
}
.inactive {
color: $muted-gray;
}
.active {
color: $active-indicator;
}
@@ -3,11 +3,13 @@ import { IoAlertCircleOutline, IoLink, IoLockClosed, IoLockOpenOutline, IoUnlink
import { TimeField, TimeStrategy } from 'ontime-types';
import { dayInMs } from 'ontime-utils';
import IconButton from '../../../common/components/buttons/IconButton';
import * as Editor from '../../../common/components/editor-utils/EditorUtils';
import TimeInput from '../../../common/components/input/time-input/TimeInput';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { cx } from '../../../common/utils/styleUtils';
import TimeInputGroup from './TimeInputGroup';
import style from './TimeInputFlow.module.scss';
@@ -67,7 +69,7 @@ function TimeInputFlow({
<>
<div>
{showLabels && <Editor.Label className={style.sectionTitle}>Start time</Editor.Label>}
<div className={cx([style.inputGroup, hasDelay && style.delayed])}>
<TimeInputGroup hasDelay={hasDelay}>
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
@@ -78,17 +80,17 @@ function TimeInputFlow({
/>
<Tooltip
text='Link start to previous end'
className={cx([style.timeAction, linkStart && style.active])}
onClick={() => handleLink(!linkStart)}
render={<IconButton variant='subtle-white' className={linkStart ? style.active : style.inactive} />}
>
<span className={style.fourtyfive}>{linkStart ? <IoLink /> : <IoUnlink />}</span>
</Tooltip>
</div>
</TimeInputGroup>
</div>
<div>
{showLabels && <Editor.Label>End time</Editor.Label>}
<div className={cx([style.inputGroup, hasDelay && style.delayed])}>
<TimeInputGroup hasDelay={hasDelay}>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
@@ -99,18 +101,18 @@ function TimeInputFlow({
/>
<Tooltip
text='Lock end'
className={cx([style.timeAction, isLockedEnd && style.active])}
render={<IconButton variant='subtle-white' className={isLockedEnd ? style.active : style.inactive} />}
onClick={() => handleChangeStrategy(TimeStrategy.LockEnd)}
data-testid='lock__end'
>
{isLockedEnd ? <IoLockClosed /> : <IoLockOpenOutline />}
</Tooltip>
</div>
</TimeInputGroup>
</div>
<div>
{showLabels && <Editor.Label>Duration</Editor.Label>}
<div className={cx([style.inputGroup, hasDelay && style.delayed])}>
<TimeInputGroup hasDelay={hasDelay}>
<TimeInput
name='duration'
submitHandler={handleSubmit}
@@ -121,13 +123,13 @@ function TimeInputFlow({
/>
<Tooltip
text='Lock duration'
className={cx([style.timeAction, isLockedDuration && style.active])}
render={<IconButton variant='subtle-white' className={isLockedDuration ? style.active : style.inactive} />}
onClick={() => handleChangeStrategy(TimeStrategy.LockDuration)}
data-testid='lock__duration'
>
{isLockedDuration ? <IoLockClosed /> : <IoLockOpenOutline />}
</Tooltip>
</div>
</TimeInputGroup>
</div>
{warnings.length > 0 && (
@@ -0,0 +1,25 @@
.inputGroup {
border: 1px solid transparent;
width: fit-content;
display: flex;
align-items: center;
border-radius: $component-border-radius-md;
&.delayed {
border: 1px solid $ontime-delay-text;
}
input {
max-width: 6.5em;
border-radius: $component-border-radius-md 0 0 $component-border-radius-md;
border: none;
padding-right: 0;
}
button {
width: 1.5rem;
height: 2rem;
border-radius: 0 $component-border-radius-md $component-border-radius-md 0;
border: none;
}
}
@@ -0,0 +1,13 @@
import { PropsWithChildren } from 'react';
import { cx } from '../../../common/utils/styleUtils';
import style from './TimeInputGroup.module.scss';
interface TimeInputGroupProps {
hasDelay?: boolean;
}
export default function TimeInputGroup({ hasDelay, children }: PropsWithChildren<TimeInputGroupProps>) {
return <div className={cx([style.inputGroup, hasDelay && style.delayed])}>{children}</div>;
}
@@ -18,6 +18,11 @@
&.skip {
position: relative;
.indexColumn {
text-decoration: line-through;
opacity: $opacity-disabled;
}
&::after {
content: '';
position: absolute;