refactor: improve editor and cuesheet headers

- consistent behaviour for app mode
- improve navigation
This commit is contained in:
Carlos Valente
2025-07-06 09:27:49 +02:00
committed by Carlos Valente
parent 79bcc179ba
commit 6facd6a666
20 changed files with 263 additions and 172 deletions
@@ -11,6 +11,11 @@
cursor: pointer; cursor: pointer;
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: 2px;
}
&:disabled { &:disabled {
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
@@ -33,5 +33,5 @@ interface SeparatorProps extends HTMLAttributes<HTMLDivElement> {
} }
export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) { export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) {
return <div className={cx([style.separator, style[orientation], className])} {...elementProps} />; return <div className={cx([style.separator, style[orientation], className])} role='separator' {...elementProps} />;
} }
@@ -1,32 +0,0 @@
import { create } from 'zustand';
export enum AppMode {
Run = 'run',
Edit = 'edit',
}
const appModeKey = 'ontime-app-mode';
function getModeFromSession() {
return sessionStorage.getItem(appModeKey) === AppMode.Run ? AppMode.Run : AppMode.Edit;
}
function persistModeToSession(mode: AppMode) {
sessionStorage.setItem(appModeKey, mode);
}
type AppModeStore = {
mode: AppMode;
setMode: (mode: AppMode) => void;
};
export const useAppMode = create<AppModeStore>()((set) => ({
mode: getModeFromSession(),
setMode: (mode: AppMode) => {
persistModeToSession(mode);
return set(() => {
return { mode };
});
},
}));
@@ -20,6 +20,11 @@ $button-color-white: $gray-50;
opacity: $opacity-disabled; opacity: $opacity-disabled;
} }
&:focus-visible {
outline: 2px solid $blue-500;
outline-offset: 2px;
}
&:hover:not(:disabled) { &:hover:not(:disabled) {
color: $button-color-white; color: $button-color-white;
background-color: $theme-color; background-color: $theme-color;
+9 -6
View File
@@ -35,9 +35,9 @@ import {
import { type EventOptions, useEntryActions } from '../../common/hooks/useEntryAction'; import { type EventOptions, useEntryActions } from '../../common/hooks/useEntryAction';
import useFollowComponent from '../../common/hooks/useFollowComponent'; import useFollowComponent from '../../common/hooks/useFollowComponent';
import { useRundownEditor } from '../../common/hooks/useSocket'; import { useRundownEditor } from '../../common/hooks/useSocket';
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
import { useEntryCopy } from '../../common/stores/entryCopyStore'; import { useEntryCopy } from '../../common/stores/entryCopyStore';
import { cloneEvent } from '../../common/utils/clone'; import { cloneEvent } from '../../common/utils/clone';
import { AppMode, sessionKeys } from '../../ontimeConfig';
import QuickAddBlock from './quick-add-block/QuickAddBlock'; import QuickAddBlock from './quick-add-block/QuickAddBlock';
import BlockEnd from './rundown-block/BlockEnd'; import BlockEnd from './rundown-block/BlockEnd';
@@ -70,12 +70,15 @@ export default function Rundown({ data }: RundownProps) {
const { entryCopyId, setEntryCopyId } = useEntryCopy(); const { entryCopyId, setEntryCopyId } = useEntryCopy();
// cursor // cursor
const { mode: appMode } = useAppMode(); const [editorMode] = useSessionStorage<AppMode>({
key: sessionKeys.editorMode,
defaultValue: AppMode.Edit,
});
const { clearSelectedEvents, setSelectedEvents, cursor } = useEventSelection(); const { clearSelectedEvents, setSelectedEvents, cursor } = useEventSelection();
const cursorRef = useRef<HTMLDivElement | null>(null); const cursorRef = useRef<HTMLDivElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null); const scrollRef = useRef<HTMLDivElement | null>(null);
useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: appMode === AppMode.Run }); useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: editorMode === AppMode.Run });
// DND KIT // DND KIT
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } })); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } }));
@@ -283,12 +286,12 @@ export default function Rundown({ data }: RundownProps) {
// in run mode, we follow selection // in run mode, we follow selection
useEffect(() => { useEffect(() => {
if (appMode !== AppMode.Run || !featureData?.selectedEventId) { if (editorMode !== AppMode.Run || !featureData?.selectedEventId) {
return; return;
} }
const index = order.findIndex((id) => id === featureData.selectedEventId); const index = order.findIndex((id) => id === featureData.selectedEventId);
setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index }); setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index });
}, [appMode, featureData.selectedEventId, order, setSelectedEvents]); }, [editorMode, featureData.selectedEventId, order, setSelectedEvents]);
/** /**
* On drag end, we reorder the events * On drag end, we reorder the events
@@ -359,7 +362,7 @@ export default function Rundown({ data }: RundownProps) {
} }
// 1. gather presentation options // 1. gather presentation options
const isEditMode = appMode === AppMode.Edit; const isEditMode = editorMode === AppMode.Edit;
// 2. initialise rundown metadata // 2. initialise rundown metadata
const { metadata, process } = makeRundownMetadata(featureData?.selectedEventId); const { metadata, process } = makeRundownMetadata(featureData?.selectedEventId);
@@ -1,4 +1,5 @@
import { memo } from 'react'; import { memo } from 'react';
import { useSessionStorage } from '@mantine/hooks';
import { ContextMenu } from '../../common/components/context-menu/ContextMenu'; import { ContextMenu } from '../../common/components/context-menu/ContextMenu';
import { Corner } from '../../common/components/editor-utils/EditorUtils'; import { Corner } from '../../common/components/editor-utils/EditorUtils';
@@ -6,9 +7,9 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu'; import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
import ProtectRoute from '../../common/components/protect-route/ProtectRoute'; import ProtectRoute from '../../common/components/protect-route/ProtectRoute';
import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice'; import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice';
import { useAppMode } from '../../common/stores/appModeStore';
import { handleLinks } from '../../common/utils/linkUtils'; import { handleLinks } from '../../common/utils/linkUtils';
import { cx } from '../../common/utils/styleUtils'; import { cx } from '../../common/utils/styleUtils';
import { AppMode, sessionKeys } from '../../ontimeConfig';
import RundownEntryEditor from './entry-editor/RundownEntryEditor'; import RundownEntryEditor from './entry-editor/RundownEntryEditor';
import FinderPlacement from './placements/FinderPlacement'; import FinderPlacement from './placements/FinderPlacement';
@@ -20,7 +21,10 @@ export default memo(RundownExport);
function RundownExport() { function RundownExport() {
const isExtracted = window.location.pathname.includes('/rundown'); const isExtracted = window.location.pathname.includes('/rundown');
const appMode = useAppMode((state) => state.mode); const [editorMode] = useSessionStorage({
key: sessionKeys.editorMode,
defaultValue: AppMode.Edit,
});
const isSmallDevice = useIsSmallDevice(); const isSmallDevice = useIsSmallDevice();
if (isSmallDevice && isExtracted) { if (isSmallDevice && isExtracted) {
@@ -45,7 +49,7 @@ function RundownExport() {
); );
} }
const hideSideBar = isExtracted && appMode === 'run'; const hideSideBar = isExtracted && editorMode === 'run';
return ( return (
<ProtectRoute permission='editor'> <ProtectRoute permission='editor'>
@@ -1,5 +1,6 @@
import { memo, useRef } from 'react'; import { memo, useRef } from 'react';
import { IoAdd } from 'react-icons/io5'; import { IoAdd } from 'react-icons/io5';
import { Toolbar } from '@base-ui-components/react/toolbar';
import { MaybeString, SupportedEntry } from 'ontime-types'; import { MaybeString, SupportedEntry } from 'ontime-types';
import Button from '../../../common/components/buttons/Button'; import Button from '../../../common/components/buttons/Button';
@@ -64,21 +65,23 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor; const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor;
return ( return (
<div className={style.quickAdd} style={blockColour ? { '--user-bg': blockColour } : {}}> <Toolbar.Root className={style.quickAdd} style={blockColour ? { '--user-bg': blockColour } : {}}>
<Button onClick={addEvent} size='small' variant='subtle-white'> <Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addEvent}>
<IoAdd /> <IoAdd />
Event Event
</Button> </Toolbar.Button>
<Button onClick={addDelay} size='small' variant='subtle-white'>
<Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addDelay}>
<IoAdd /> <IoAdd />
Delay Delay
</Button> </Toolbar.Button>
{parentBlock === null && ( {parentBlock === null && (
<Button onClick={addBlock} size='small' variant='subtle-white'> <Toolbar.Button render={<Button size='small' variant='subtle-white' />} onClick={addBlock}>
<IoAdd /> <IoAdd />
Block Block
</Button> </Toolbar.Button>
)} )}
</div> </Toolbar.Root>
); );
} }
@@ -9,6 +9,7 @@
} }
} }
/** styles should match CuesheetTableSettings.module.scss */
.group { .group {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -19,7 +20,8 @@
height: 2rem; height: 2rem;
} }
.button { /** styles should match CuesheetTableSettings.module.scss */
.radioButton {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -34,6 +36,7 @@
&:focus-visible { &:focus-visible {
background: transparent; background: transparent;
outline: 1px solid $blue-500;
} }
&:hover:not(:disabled):not(:active) { &:hover:not(:disabled):not(:active) {
@@ -2,11 +2,12 @@ import { memo } from 'react';
import { Toggle } from '@base-ui-components/react/toggle'; import { Toggle } from '@base-ui-components/react/toggle';
import { ToggleGroup } from '@base-ui-components/react/toggle-group'; import { ToggleGroup } from '@base-ui-components/react/toggle-group';
import { Toolbar } from '@base-ui-components/react/toolbar'; import { Toolbar } from '@base-ui-components/react/toolbar';
import { useSessionStorage } from '@mantine/hooks';
import { OffsetMode } from 'ontime-types'; import { OffsetMode } from 'ontime-types';
import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import * as Editor from '../../../common/components/editor-utils/EditorUtils';
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket'; import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
import { AppMode, useAppMode } from '../../../common/stores/appModeStore'; import { AppMode, sessionKeys } from '../../../ontimeConfig';
import RundownMenu from './RundownMenu'; import RundownMenu from './RundownMenu';
@@ -14,8 +15,7 @@ import style from './RundownHeader.module.scss';
export default memo(RundownHeader); export default memo(RundownHeader);
function RundownHeader() { function RundownHeader() {
const appMode = useAppMode((state) => state.mode); const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit });
const setAppMode = useAppMode((state) => state.setMode);
const { offsetMode } = useOffsetMode(); const { offsetMode } = useOffsetMode();
@@ -23,7 +23,7 @@ function RundownHeader() {
// we need to stop user from deselecting a mode // we need to stop user from deselecting a mode
const newValue = mode.at(0); const newValue = mode.at(0);
if (!newValue) return; if (!newValue) return;
setAppMode(newValue); setEditorMode(newValue);
}; };
const toggleOffsetMode = (mode: OffsetMode[]) => { const toggleOffsetMode = (mode: OffsetMode[]) => {
@@ -35,11 +35,11 @@ function RundownHeader() {
return ( return (
<Toolbar.Root className={style.header}> <Toolbar.Root className={style.header}>
<ToggleGroup value={[appMode]} onValueChange={toggleAppMode} className={style.group}> <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 Run
</Toolbar.Button> </Toolbar.Button>
<Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.button}> <Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.radioButton}>
Edit Edit
</Toolbar.Button> </Toolbar.Button>
</ToggleGroup> </ToggleGroup>
@@ -47,10 +47,10 @@ function RundownHeader() {
<Editor.Separator className={style.separator} /> <Editor.Separator className={style.separator} />
<ToggleGroup value={[offsetMode]} onValueChange={toggleOffsetMode} className={style.group}> <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 Absolute
</Toolbar.Button> </Toolbar.Button>
<Toolbar.Button render={<Toggle />} value={OffsetMode.Relative} className={style.button}> <Toolbar.Button render={<Toggle />} value={OffsetMode.Relative} className={style.radioButton}>
Relative Relative
</Toolbar.Button> </Toolbar.Button>
</ToggleGroup> </ToggleGroup>
@@ -2,18 +2,21 @@ import { memo } from 'react';
import { Toggle } from '@base-ui-components/react/toggle'; import { Toggle } from '@base-ui-components/react/toggle';
import { ToggleGroup } from '@base-ui-components/react/toggle-group'; import { ToggleGroup } from '@base-ui-components/react/toggle-group';
import { Toolbar } from '@base-ui-components/react/toolbar'; import { Toolbar } from '@base-ui-components/react/toolbar';
import { useSessionStorage } from '@mantine/hooks';
import { OffsetMode } from 'ontime-types'; import { OffsetMode } from 'ontime-types';
import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import * as Editor from '../../../common/components/editor-utils/EditorUtils';
import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket'; import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket';
import { AppMode, useAppMode } from '../../../common/stores/appModeStore'; import { AppMode, sessionKeys } from '../../../ontimeConfig';
import style from './RundownHeader.module.scss'; import style from './RundownHeader.module.scss';
export default memo(RundownHeader); export default memo(RundownHeader);
function RundownHeader() { function RundownHeader() {
const appMode = useAppMode((state) => state.mode); const [editorMode, setEditorMode] = useSessionStorage<AppMode>({
const setAppMode = useAppMode((state) => state.setMode); key: sessionKeys.editorMode,
defaultValue: AppMode.Edit,
});
const { offsetMode } = useOffsetMode(); const { offsetMode } = useOffsetMode();
@@ -21,7 +24,7 @@ function RundownHeader() {
// we need to stop user from deselecting a mode // we need to stop user from deselecting a mode
const newValue = mode.at(0); const newValue = mode.at(0);
if (!newValue) return; if (!newValue) return;
setAppMode(newValue); setEditorMode(newValue);
}; };
const toggleOffsetMode = (mode: OffsetMode[]) => { const toggleOffsetMode = (mode: OffsetMode[]) => {
@@ -33,7 +36,7 @@ function RundownHeader() {
return ( return (
<Toolbar.Root className={style.header}> <Toolbar.Root className={style.header}>
<ToggleGroup value={[appMode]} onValueChange={toggleAppMode} className={style.group}> <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.button}>
Run Run
</Toolbar.Button> </Toolbar.Button>
@@ -1,11 +1,12 @@
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { IoTrash } from 'react-icons/io5'; import { IoTrash } from 'react-icons/io5';
import { useDisclosure } from '@mantine/hooks'; import { Toolbar } from '@base-ui-components/react/toolbar';
import { useDisclosure, useSessionStorage } from '@mantine/hooks';
import Button from '../../../common/components/buttons/Button'; import Button from '../../../common/components/buttons/Button';
import Dialog from '../../../common/components/dialog/Dialog'; import Dialog from '../../../common/components/dialog/Dialog';
import { useEntryActions } from '../../../common/hooks/useEntryAction'; import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { AppMode, useAppMode } from '../../../common/stores/appModeStore'; import { AppMode, sessionKeys } from '../../../ontimeConfig';
import { useEventSelection } from '../useEventSelection'; import { useEventSelection } from '../useEventSelection';
import style from './RundownHeader.module.scss'; import style from './RundownHeader.module.scss';
@@ -15,7 +16,10 @@ function RundownMenu() {
const [isOpen, handlers] = useDisclosure(); const [isOpen, handlers] = useDisclosure();
const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents); const clearSelectedEvents = useEventSelection((state) => state.clearSelectedEvents);
const appMode = useAppMode((state) => state.mode); const [editorMode] = useSessionStorage({
key: sessionKeys.cuesheetMode,
defaultValue: AppMode.Edit,
});
const { deleteAllEntries } = useEntryActions(); const { deleteAllEntries } = useEntryActions();
const deleteAll = useCallback(() => { const deleteAll = useCallback(() => {
@@ -26,15 +30,15 @@ function RundownMenu() {
return ( return (
<> <>
<Button <Toolbar.Button
variant='subtle-destructive' render={<Button variant='subtle-destructive' />}
onClick={handlers.open}
disabled={editorMode === AppMode.Run}
className={style.apart} className={style.apart}
onClick={() => handlers.open()}
disabled={appMode === AppMode.Run}
> >
<IoTrash /> <IoTrash />
Clear all Clear all
</Button> </Toolbar.Button>
<Dialog <Dialog
isOpen={isOpen} isOpen={isOpen}
onClose={handlers.close} onClose={handlers.close}
@@ -48,7 +52,7 @@ function RundownMenu() {
} }
footerElements={ footerElements={
<> <>
<Button size='large' onClick={handlers.close}> <Button variant='ghosted-white' size='large' onClick={handlers.close}>
Cancel Cancel
</Button> </Button>
<Button variant='destructive' size='large' onClick={deleteAll}> <Button variant='destructive' size='large' onClick={deleteAll}>
+10
View File
@@ -4,3 +4,13 @@ export const tooltipDelayFast = 300;
export const queryRefetchInterval = 60000; // 60 seconds export const queryRefetchInterval = 60000; // 60 seconds
export const queryRefetchIntervalSlow = 120000; // 120 seconds export const queryRefetchIntervalSlow = 120000; // 120 seconds
export const sessionKeys = {
cuesheetMode: 'cuesheet-mode',
editorMode: 'editor-mode',
};
export enum AppMode {
Run = 'run',
Edit = 'edit',
}
@@ -1,10 +1,12 @@
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useSessionStorage } from '@mantine/hooks';
import { useTableNav } from '@table-nav/react'; import { useTableNav } from '@table-nav/react';
import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table';
import { OntimeEntry, TimeField } from 'ontime-types'; import { OntimeEntry, TimeField } from 'ontime-types';
import { useEntryActions } from '../../../common/hooks/useEntryAction'; import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { useFollowSelected } from '../../../common/hooks/useFollowComponent'; import { useFollowSelected } from '../../../common/hooks/useFollowComponent';
import { AppMode,sessionKeys } from '../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../cuesheet.options'; import { usePersistedCuesheetOptions } from '../cuesheet.options';
import CuesheetBody from './cuesheet-table-elements/CuesheetBody'; import CuesheetBody from './cuesheet-table-elements/CuesheetBody';
@@ -22,11 +24,14 @@ interface CuesheetTableProps {
export default function CuesheetTable({ data, columns }: CuesheetTableProps) { export default function CuesheetTable({ data, columns }: CuesheetTableProps) {
const { updateEntry, updateTimer } = useEntryActions(); const { updateEntry, updateTimer } = useEntryActions();
const followPlayback = usePersistedCuesheetOptions((state) => state.followPlayback);
const showDelayedTimes = usePersistedCuesheetOptions((state) => state.showDelayedTimes); const showDelayedTimes = usePersistedCuesheetOptions((state) => state.showDelayedTimes);
const hideTableSeconds = usePersistedCuesheetOptions((state) => state.hideTableSeconds); const hideTableSeconds = usePersistedCuesheetOptions((state) => state.hideTableSeconds);
const [cuesheetMode] = useSessionStorage({
key: sessionKeys.cuesheetMode,
defaultValue: AppMode.Edit,
});
const { selectedRef, scrollRef } = useFollowSelected(followPlayback); const { selectedRef, scrollRef } = useFollowSelected(cuesheetMode === AppMode.Run);
const { listeners } = useTableNav(); const { listeners } = useTableNav();
@@ -1,9 +1,11 @@
import { IoEllipsisHorizontal } from 'react-icons/io5'; import { IoEllipsisHorizontal } from 'react-icons/io5';
import { useSessionStorage } from '@mantine/hooks';
import { flexRender, Table } from '@tanstack/react-table'; import { flexRender, Table } from '@tanstack/react-table';
import { EntryId, OntimeEntry } from 'ontime-types'; import { EntryId, OntimeEntry } from 'ontime-types';
import IconButton from '../../../../common/components/buttons/IconButton'; import IconButton from '../../../../common/components/buttons/IconButton';
import { useCurrentBlockId } from '../../../../common/hooks/useSocket'; import { useCurrentBlockId } from '../../../../common/hooks/useSocket';
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../../cuesheet.options'; import { usePersistedCuesheetOptions } from '../../cuesheet.options';
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu'; import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
@@ -22,7 +24,10 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
const { currentBlockId } = useCurrentBlockId(); const { currentBlockId } = useCurrentBlockId();
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn); const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
const showActionMenu = usePersistedCuesheetOptions((state) => state.showActionMenu); const [cuesheetMode] = useSessionStorage<AppMode>({
key: sessionKeys.cuesheetMode,
defaultValue: AppMode.Edit,
});
const openMenu = useCuesheetTableMenu((store) => store.openMenu); const openMenu = useCuesheetTableMenu((store) => store.openMenu);
if (hidePast && !currentBlockId) { if (hidePast && !currentBlockId) {
@@ -31,7 +36,7 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
return ( return (
<tr className={style.blockRow} style={{ '--user-bg': colour }} data-testid='cuesheet-block'> <tr className={style.blockRow} style={{ '--user-bg': colour }} data-testid='cuesheet-block'>
{showActionMenu && ( {cuesheetMode === AppMode.Edit && (
<td className={style.actionColumn} tabIndex={-1} role='cell'> <td className={style.actionColumn} tabIndex={-1} role='cell'>
<IconButton <IconButton
aria-label='Options' aria-label='Options'
@@ -1,8 +1,10 @@
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable'; import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable';
import { useSessionStorage } from '@mantine/hooks';
import { flexRender, HeaderGroup } from '@tanstack/react-table'; import { flexRender, HeaderGroup } from '@tanstack/react-table';
import { OntimeEntry } from 'ontime-types'; import { OntimeEntry } from 'ontime-types';
import { getAccessibleColour } from '../../../../common/utils/styleUtils'; import { getAccessibleColour } from '../../../../common/utils/styleUtils';
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../../cuesheet.options'; import { usePersistedCuesheetOptions } from '../../cuesheet.options';
import { SortableCell } from './SortableCell'; import { SortableCell } from './SortableCell';
@@ -15,7 +17,10 @@ interface CuesheetHeaderProps {
export default function CuesheetHeader({ headerGroups }: CuesheetHeaderProps) { export default function CuesheetHeader({ headerGroups }: CuesheetHeaderProps) {
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn); const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
const showActionMenu = usePersistedCuesheetOptions((state) => state.showActionMenu); const [cuesheetMode] = useSessionStorage<AppMode>({
key: sessionKeys.cuesheetMode,
defaultValue: AppMode.Edit,
});
return ( return (
<thead className={style.tableHeader}> <thead className={style.tableHeader}>
@@ -24,7 +29,7 @@ export default function CuesheetHeader({ headerGroups }: CuesheetHeaderProps) {
return ( return (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{showActionMenu && <th className={style.actionColumn} tabIndex={-1} />} {cuesheetMode === AppMode.Edit && <th className={style.actionColumn} tabIndex={-1} />}
{!hideIndexColumn && ( {!hideIndexColumn && (
<th className={style.indexColumn} tabIndex={-1}> <th className={style.indexColumn} tabIndex={-1}>
# #
@@ -1,11 +1,13 @@
import { MutableRefObject, useEffect, useRef } from 'react'; import { MutableRefObject, useEffect, useRef } from 'react';
import { IoEllipsisHorizontal } from 'react-icons/io5'; import { IoEllipsisHorizontal } from 'react-icons/io5';
import { useSessionStorage } from '@mantine/hooks';
import { flexRender, Table } from '@tanstack/react-table'; import { flexRender, Table } from '@tanstack/react-table';
import { OntimeEntry, OntimeEvent, RGBColour } from 'ontime-types'; import { OntimeEntry, OntimeEvent, RGBColour } from 'ontime-types';
import { colourToHex, cssOrHexToColour } from 'ontime-utils'; import { colourToHex, cssOrHexToColour } from 'ontime-utils';
import IconButton from '../../../../common/components/buttons/IconButton'; import IconButton from '../../../../common/components/buttons/IconButton';
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils'; import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../../cuesheet.options'; import { usePersistedCuesheetOptions } from '../../cuesheet.options';
import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu'; import { useCuesheetTableMenu } from '../cuesheet-table-menu/useCuesheetTableMenu';
@@ -44,8 +46,10 @@ export default function EventRow({
firstAfterBlock, firstAfterBlock,
}: EventRowProps) { }: EventRowProps) {
const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn); const hideIndexColumn = usePersistedCuesheetOptions((state) => state.hideIndexColumn);
const showActionMenu = usePersistedCuesheetOptions((state) => state.showActionMenu); const [cuesheetMode] = useSessionStorage<AppMode>({
const ownRef = useRef<HTMLTableRowElement>(null); key: sessionKeys.cuesheetMode,
defaultValue: AppMode.Edit,
}); const ownRef = useRef<HTMLTableRowElement>(null);
const isVisible = useVisibleRowsStore((state) => state.visibleRows.has(rowId)); const isVisible = useVisibleRowsStore((state) => state.visibleRows.has(rowId));
@@ -86,7 +90,7 @@ export default function EventRow({
ref={selectedRef ?? ownRef} ref={selectedRef ?? ownRef}
data-testid='cuesheet-event' data-testid='cuesheet-event'
> >
{showActionMenu && ( {cuesheetMode === AppMode.Edit && (
<td className={style.actionColumn} tabIndex={-1} role='cell'> <td className={style.actionColumn} tabIndex={-1} role='cell'>
<IconButton <IconButton
aria-label='Options' aria-label='Options'
@@ -1,3 +1,4 @@
import { Toolbar } from '@base-ui-components/react/toolbar';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import Button from '../../../../common/components/buttons/Button'; import Button from '../../../../common/components/buttons/Button';
@@ -17,10 +18,10 @@ function CuesheetShareModal() {
return ( return (
<> <>
<Button variant='subtle' onClick={handler.open}> <Toolbar.Button onClick={handler.open} render={<Button />}>
<RotatedLink /> <RotatedLink />
Share... Share...
</Button> </Toolbar.Button>
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onClose={handler.close} onClose={handler.close}
@@ -5,7 +5,7 @@
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 1rem;
font-size: $inner-section-text-size; font-size: $inner-section-text-size;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
} }
@@ -39,4 +39,56 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
} }
.apart {
margin-left: auto;
}
/** styles should match RundownHeader.module.scss */
.group {
display: flex;
align-items: center;
gap: 2px;
padding-inline: 2px;
background: $gray-1100;
border-radius: $component-border-radius-md;
height: 2rem;
}
/** styles should match RundownHeader.module.scss */
.radioButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: calc(2rem - 4px); // padding inline * 2
padding-inline: 1em;
border: 1px solid transparent;
border-radius: $component-border-radius-md;
color: $gray-400;
font-size: calc(1rem - 2px);
font-weight: 600;
&:focus-visible {
background: transparent;
outline: 1px solid $blue-500;
}
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
}
&:active {
background: $gray-1100;
}
&[data-pressed] {
background: $blue-700;
color: $ui-white;
&:hover:not(:disabled):not(:active) {
background: $blue-600;
}
}
}
@@ -1,6 +1,10 @@
import { memo, ReactNode } from 'react'; import { memo, ReactNode } from 'react';
import { IoChevronDown, IoLocate, IoOptions, IoSettingsOutline } from 'react-icons/io5'; import { IoChevronDown, IoOptions, IoSettingsOutline } from 'react-icons/io5';
import { Popover } from '@base-ui-components/react/popover'; import { Popover } from '@base-ui-components/react/popover';
import { Toggle } from '@base-ui-components/react/toggle';
import { ToggleGroup } from '@base-ui-components/react/toggle-group';
import { Toolbar } from '@base-ui-components/react/toolbar';
import { useSessionStorage } from '@mantine/hooks';
import type { Column } from '@tanstack/react-table'; import type { Column } from '@tanstack/react-table';
import { OntimeEntry } from 'ontime-types'; import { OntimeEntry } from 'ontime-types';
@@ -8,6 +12,8 @@ import Button from '../../../../common/components/buttons/Button';
import Checkbox from '../../../../common/components/checkbox/Checkbox'; import Checkbox from '../../../../common/components/checkbox/Checkbox';
import * as Editor from '../../../../common/components/editor-utils/EditorUtils'; import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
import PopoverContents from '../../../../common/components/popover/Popover'; import PopoverContents from '../../../../common/components/popover/Popover';
import { cx } from '../../../../common/utils/styleUtils';
import { AppMode, sessionKeys } from '../../../../ontimeConfig';
import { usePersistedCuesheetOptions } from '../../cuesheet.options'; import { usePersistedCuesheetOptions } from '../../cuesheet.options';
import CuesheetShareModal from './CuesheetShareModal'; import CuesheetShareModal from './CuesheetShareModal';
@@ -28,36 +34,40 @@ function CuesheetTableSettings({
handleResetReordering, handleResetReordering,
handleClearToggles, handleClearToggles,
}: CuesheetTableSettingsProps) { }: CuesheetTableSettingsProps) {
return ( const [cuesheetMode, setCuesheetMode] = useSessionStorage({
<div className={style.tableSettings}> key: sessionKeys.cuesheetMode,
<div className={style.inline}> defaultValue: AppMode.Edit,
<ViewSettings /> });
<ColumnSettings
columns={columns}
handleResetResizing={handleResetResizing}
handleResetReordering={handleResetReordering}
handleClearToggles={handleClearToggles}
/>
</div>
<div className={style.inline}> const toggleCuesheetMode = (mode: AppMode[]) => {
<ViewSettingsFollowButton /> // we need to stop user from deselecting a mode
<Editor.Separator orientation='vertical' /> const newValue = mode.at(0);
<CuesheetShareModal /> if (!newValue) return;
</div> setCuesheetMode(newValue);
</div> };
);
}
function ViewSettingsFollowButton() {
const followPlayback = usePersistedCuesheetOptions((state) => state.followPlayback);
const toggle = usePersistedCuesheetOptions((state) => state.toggleOption);
return ( return (
<Button variant={followPlayback ? 'primary' : 'subtle'} onClick={() => toggle('followPlayback')}> <Toolbar.Root className={style.tableSettings}>
<IoLocate /> <ViewSettings />
{followPlayback ? 'Following playback' : 'Follow playback'} <ColumnSettings
</Button> columns={columns}
handleResetResizing={handleResetResizing}
handleResetReordering={handleResetReordering}
handleClearToggles={handleClearToggles}
/>
<ToggleGroup value={[cuesheetMode]} onValueChange={toggleCuesheetMode} className={cx([style.group, style.apart])}>
<Toolbar.Button render={<Toggle />} value={AppMode.Run} className={style.radioButton}>
Run
</Toolbar.Button>
<Toolbar.Button render={<Toggle />} value={AppMode.Edit} className={style.radioButton}>
Edit
</Toolbar.Button>
</ToggleGroup>
<Editor.Separator orientation='vertical' />
<CuesheetShareModal />
</Toolbar.Root>
); );
} }
@@ -68,59 +78,60 @@ function ViewSettings() {
<Popover.Root> <Popover.Root>
<Popover.Trigger <Popover.Trigger
render={ render={
<Button variant='ghosted-white'> <Toolbar.Button
<IoSettingsOutline /> Settings render={
<IoChevronDown /> <Button variant='ghosted-white'>
</Button> <IoSettingsOutline /> Settings
<IoChevronDown />
</Button>
}
/>
} }
/> />
<PopoverContents align='start' className={style.column}> <PopoverContents align='start' className={style.inline}>
<Editor.Label className={style.sectionTitle}>Element visibility</Editor.Label> <div className={style.column}>
<Editor.Label className={style.option}> <Editor.Label className={style.sectionTitle}>Element visibility</Editor.Label>
<Checkbox <Editor.Label className={style.option}>
defaultChecked={options.showActionMenu} <Checkbox
onCheckedChange={(checked) => options.setOption('showActionMenu', checked)} defaultChecked={options.hideTableSeconds}
/> onCheckedChange={(checked) => options.setOption('hideTableSeconds', checked)}
Show action menu />
</Editor.Label> Hide seconds in table
<Editor.Label className={style.option}> </Editor.Label>
<Checkbox <Editor.Label className={style.option}>
defaultChecked={options.hideTableSeconds} <Checkbox
onCheckedChange={(checked) => options.setOption('hideTableSeconds', checked)} defaultChecked={options.hidePast}
/> onCheckedChange={(checked) => options.setOption('hidePast', checked)}
Hide seconds in table />
</Editor.Label> Hide past events
<Editor.Label className={style.option}> </Editor.Label>
<Checkbox <Editor.Label className={style.option}>
defaultChecked={options.hidePast} <Checkbox
onCheckedChange={(checked) => options.setOption('hidePast', checked)} defaultChecked={options.hideIndexColumn}
/> onCheckedChange={(checked) => options.setOption('hideIndexColumn', checked)}
Hide past events />
</Editor.Label> Hide index column
<Editor.Label className={style.option}> </Editor.Label>
<Checkbox </div>
defaultChecked={options.hideIndexColumn}
onCheckedChange={(checked) => options.setOption('hideIndexColumn', checked)}
/>
Hide index column
</Editor.Label>
<Editor.Label className={style.sectionTitle}>Table Behaviour</Editor.Label> <div className={style.column}>
<Editor.Label className={style.option}> <Editor.Label className={style.sectionTitle}>Table Behaviour</Editor.Label>
<Checkbox <Editor.Label className={style.option}>
defaultChecked={options.showDelayedTimes} <Checkbox
onCheckedChange={(checked) => options.setOption('showDelayedTimes', checked)} defaultChecked={options.showDelayedTimes}
/> onCheckedChange={(checked) => options.setOption('showDelayedTimes', checked)}
Show delayed times />
</Editor.Label> Show delayed times
<Editor.Label className={style.option}> </Editor.Label>
<Checkbox <Editor.Label className={style.option}>
defaultChecked={options.hideDelays} <Checkbox
onCheckedChange={(checked) => options.setOption('hideDelays', checked)} defaultChecked={options.hideDelays}
/> onCheckedChange={(checked) => options.setOption('hideDelays', checked)}
Hide delay entries />
</Editor.Label> Hide delay entries
</Editor.Label>
</div>
</PopoverContents> </PopoverContents>
</Popover.Root> </Popover.Root>
); );
@@ -136,10 +147,14 @@ function ColumnSettings({
<Popover.Root> <Popover.Root>
<Popover.Trigger <Popover.Trigger
render={ render={
<Button variant='ghosted-white'> <Toolbar.Button
<IoOptions /> View render={
<IoChevronDown /> <Button variant='ghosted-white'>
</Button> <IoOptions /> View
<IoChevronDown />
</Button>
}
/>
} }
/> />
<PopoverContents align='start' className={style.inline}> <PopoverContents align='start' className={style.inline}>
@@ -2,9 +2,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
type OptionValues = { type OptionValues = {
showActionMenu: boolean;
hideTableSeconds: boolean; hideTableSeconds: boolean;
followPlayback: boolean;
hidePast: boolean; hidePast: boolean;
hideIndexColumn: boolean; hideIndexColumn: boolean;
showDelayedTimes: boolean; showDelayedTimes: boolean;
@@ -12,9 +10,7 @@ type OptionValues = {
}; };
const defaultOptions: OptionValues = { const defaultOptions: OptionValues = {
showActionMenu: false,
hideTableSeconds: false, hideTableSeconds: false,
followPlayback: false,
hidePast: false, hidePast: false,
hideIndexColumn: false, hideIndexColumn: false,
showDelayedTimes: false, showDelayedTimes: false,