From 6facd6a666ab744071e9985d8843f7f89398b162 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 6 Jul 2025 09:27:49 +0200 Subject: [PATCH] refactor: improve editor and cuesheet headers - consistent behaviour for app mode - improve navigation --- .../components/buttons/IconButton.module.scss | 5 + .../components/editor-utils/EditorUtils.tsx | 2 +- apps/client/src/common/stores/appModeStore.ts | 32 ---- .../playback/tap-button/TapButton.module.scss | 5 + apps/client/src/features/rundown/Rundown.tsx | 15 +- .../src/features/rundown/RundownExport.tsx | 10 +- .../rundown/quick-add-block/QuickAddBlock.tsx | 19 +- .../rundown-header/RundownHeader.module.scss | 5 +- .../rundown/rundown-header/RundownHeader.tsx | 18 +- .../rundown-header/RundownHeaderMobile.tsx | 13 +- .../rundown/rundown-header/RundownMenu.tsx | 22 ++- apps/client/src/ontimeConfig.ts | 10 + .../cuesheet/cuesheet-table/CuesheetTable.tsx | 9 +- .../cuesheet-table-elements/BlockRow.tsx | 9 +- .../CuesheetHeader.tsx | 9 +- .../cuesheet-table-elements/EventRow.tsx | 10 +- .../CuesheetShareModal.tsx | 5 +- .../CuesheetTableSettings.module.scss | 56 +++++- .../CuesheetTableSettings.tsx | 177 ++++++++++-------- .../src/views/cuesheet/cuesheet.options.ts | 4 - 20 files changed, 263 insertions(+), 172 deletions(-) delete mode 100644 apps/client/src/common/stores/appModeStore.ts diff --git a/apps/client/src/common/components/buttons/IconButton.module.scss b/apps/client/src/common/components/buttons/IconButton.module.scss index 1b10acad6..06960483d 100644 --- a/apps/client/src/common/components/buttons/IconButton.module.scss +++ b/apps/client/src/common/components/buttons/IconButton.module.scss @@ -11,6 +11,11 @@ cursor: pointer; + &:focus-visible { + outline: 2px solid $blue-500; + outline-offset: 2px; + } + &:disabled { opacity: 0.4; cursor: not-allowed; diff --git a/apps/client/src/common/components/editor-utils/EditorUtils.tsx b/apps/client/src/common/components/editor-utils/EditorUtils.tsx index 09c686fea..4737fe905 100644 --- a/apps/client/src/common/components/editor-utils/EditorUtils.tsx +++ b/apps/client/src/common/components/editor-utils/EditorUtils.tsx @@ -33,5 +33,5 @@ interface SeparatorProps extends HTMLAttributes { } export function Separator({ className, orientation = 'vertical', ...elementProps }: SeparatorProps) { - return
; + return
; } diff --git a/apps/client/src/common/stores/appModeStore.ts b/apps/client/src/common/stores/appModeStore.ts deleted file mode 100644 index b40d18c78..000000000 --- a/apps/client/src/common/stores/appModeStore.ts +++ /dev/null @@ -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()((set) => ({ - mode: getModeFromSession(), - setMode: (mode: AppMode) => { - persistModeToSession(mode); - - return set(() => { - return { mode }; - }); - }, -})); diff --git a/apps/client/src/features/control/playback/tap-button/TapButton.module.scss b/apps/client/src/features/control/playback/tap-button/TapButton.module.scss index 38585de59..e091dbbb7 100644 --- a/apps/client/src/features/control/playback/tap-button/TapButton.module.scss +++ b/apps/client/src/features/control/playback/tap-button/TapButton.module.scss @@ -20,6 +20,11 @@ $button-color-white: $gray-50; opacity: $opacity-disabled; } + &:focus-visible { + outline: 2px solid $blue-500; + outline-offset: 2px; + } + &:hover:not(:disabled) { color: $button-color-white; background-color: $theme-color; diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx index 99c865f71..afd61774b 100644 --- a/apps/client/src/features/rundown/Rundown.tsx +++ b/apps/client/src/features/rundown/Rundown.tsx @@ -35,9 +35,9 @@ import { import { type EventOptions, useEntryActions } from '../../common/hooks/useEntryAction'; import useFollowComponent from '../../common/hooks/useFollowComponent'; import { useRundownEditor } from '../../common/hooks/useSocket'; -import { AppMode, useAppMode } from '../../common/stores/appModeStore'; import { useEntryCopy } from '../../common/stores/entryCopyStore'; import { cloneEvent } from '../../common/utils/clone'; +import { AppMode, sessionKeys } from '../../ontimeConfig'; import QuickAddBlock from './quick-add-block/QuickAddBlock'; import BlockEnd from './rundown-block/BlockEnd'; @@ -70,12 +70,15 @@ export default function Rundown({ data }: RundownProps) { const { entryCopyId, setEntryCopyId } = useEntryCopy(); // cursor - const { mode: appMode } = useAppMode(); + const [editorMode] = useSessionStorage({ + key: sessionKeys.editorMode, + defaultValue: AppMode.Edit, + }); const { clearSelectedEvents, setSelectedEvents, cursor } = useEventSelection(); const cursorRef = useRef(null); const scrollRef = useRef(null); - useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: appMode === AppMode.Run }); + useFollowComponent({ followRef: cursorRef, scrollRef, doFollow: editorMode === AppMode.Run }); // DND KIT const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } })); @@ -283,12 +286,12 @@ export default function Rundown({ data }: RundownProps) { // in run mode, we follow selection useEffect(() => { - if (appMode !== AppMode.Run || !featureData?.selectedEventId) { + if (editorMode !== AppMode.Run || !featureData?.selectedEventId) { return; } const index = order.findIndex((id) => id === featureData.selectedEventId); setSelectedEvents({ id: featureData.selectedEventId, selectMode: 'click', index }); - }, [appMode, featureData.selectedEventId, order, setSelectedEvents]); + }, [editorMode, featureData.selectedEventId, order, setSelectedEvents]); /** * On drag end, we reorder the events @@ -359,7 +362,7 @@ export default function Rundown({ data }: RundownProps) { } // 1. gather presentation options - const isEditMode = appMode === AppMode.Edit; + const isEditMode = editorMode === AppMode.Edit; // 2. initialise rundown metadata const { metadata, process } = makeRundownMetadata(featureData?.selectedEventId); diff --git a/apps/client/src/features/rundown/RundownExport.tsx b/apps/client/src/features/rundown/RundownExport.tsx index d35b1e1bc..3073c3a3f 100644 --- a/apps/client/src/features/rundown/RundownExport.tsx +++ b/apps/client/src/features/rundown/RundownExport.tsx @@ -1,4 +1,5 @@ import { memo } from 'react'; +import { useSessionStorage } from '@mantine/hooks'; import { ContextMenu } from '../../common/components/context-menu/ContextMenu'; 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 ProtectRoute from '../../common/components/protect-route/ProtectRoute'; import { useIsSmallDevice } from '../../common/hooks/useIsSmallDevice'; -import { useAppMode } from '../../common/stores/appModeStore'; import { handleLinks } from '../../common/utils/linkUtils'; import { cx } from '../../common/utils/styleUtils'; +import { AppMode, sessionKeys } from '../../ontimeConfig'; import RundownEntryEditor from './entry-editor/RundownEntryEditor'; import FinderPlacement from './placements/FinderPlacement'; @@ -20,7 +21,10 @@ export default memo(RundownExport); function RundownExport() { 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(); if (isSmallDevice && isExtracted) { @@ -45,7 +49,7 @@ function RundownExport() { ); } - const hideSideBar = isExtracted && appMode === 'run'; + const hideSideBar = isExtracted && editorMode === 'run'; return ( diff --git a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx index 66fe499f9..b88a5bcc4 100644 --- a/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx +++ b/apps/client/src/features/rundown/quick-add-block/QuickAddBlock.tsx @@ -1,5 +1,6 @@ import { memo, useRef } from 'react'; import { IoAdd } from 'react-icons/io5'; +import { Toolbar } from '@base-ui-components/react/toolbar'; import { MaybeString, SupportedEntry } from 'ontime-types'; import Button from '../../../common/components/buttons/Button'; @@ -64,21 +65,23 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor; return ( -
- - + + {parentBlock === null && ( - + )} -
+ ); } diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss b/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss index fca79357e..a2dcd61fe 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.module.scss @@ -9,6 +9,7 @@ } } +/** styles should match CuesheetTableSettings.module.scss */ .group { display: flex; align-items: center; @@ -19,7 +20,8 @@ height: 2rem; } -.button { +/** styles should match CuesheetTableSettings.module.scss */ +.radioButton { box-sizing: border-box; display: flex; align-items: center; @@ -34,6 +36,7 @@ &:focus-visible { background: transparent; + outline: 1px solid $blue-500; } &:hover:not(:disabled):not(:active) { diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx index d7152fd5b..ebd568f20 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeader.tsx @@ -2,11 +2,12 @@ import { memo } from 'react'; 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 { OffsetMode } from 'ontime-types'; import * as Editor from '../../../common/components/editor-utils/EditorUtils'; import { setOffsetMode, useOffsetMode } from '../../../common/hooks/useSocket'; -import { AppMode, useAppMode } from '../../../common/stores/appModeStore'; +import { AppMode, sessionKeys } from '../../../ontimeConfig'; import RundownMenu from './RundownMenu'; @@ -14,8 +15,7 @@ import style from './RundownHeader.module.scss'; export default memo(RundownHeader); function RundownHeader() { - const appMode = useAppMode((state) => state.mode); - const setAppMode = useAppMode((state) => state.setMode); + const [editorMode, setEditorMode] = useSessionStorage({ key: sessionKeys.editorMode, defaultValue: AppMode.Edit }); const { offsetMode } = useOffsetMode(); @@ -23,7 +23,7 @@ function RundownHeader() { // we need to stop user from deselecting a mode const newValue = mode.at(0); if (!newValue) return; - setAppMode(newValue); + setEditorMode(newValue); }; const toggleOffsetMode = (mode: OffsetMode[]) => { @@ -35,11 +35,11 @@ function RundownHeader() { return ( - - } value={AppMode.Run} className={style.button}> + + } value={AppMode.Run} className={style.radioButton}> Run - } value={AppMode.Edit} className={style.button}> + } value={AppMode.Edit} className={style.radioButton}> Edit @@ -47,10 +47,10 @@ function RundownHeader() { - } value={OffsetMode.Absolute} className={style.button}> + } value={OffsetMode.Absolute} className={style.radioButton}> Absolute - } value={OffsetMode.Relative} className={style.button}> + } value={OffsetMode.Relative} className={style.radioButton}> Relative diff --git a/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx b/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx index f3f1aec1d..ea7912516 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownHeaderMobile.tsx @@ -2,18 +2,21 @@ import { memo } from 'react'; 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 { OffsetMode } from 'ontime-types'; import * as Editor from '../../../common/components/editor-utils/EditorUtils'; 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'; export default memo(RundownHeader); function RundownHeader() { - const appMode = useAppMode((state) => state.mode); - const setAppMode = useAppMode((state) => state.setMode); + const [editorMode, setEditorMode] = useSessionStorage({ + key: sessionKeys.editorMode, + defaultValue: AppMode.Edit, + }); const { offsetMode } = useOffsetMode(); @@ -21,7 +24,7 @@ function RundownHeader() { // we need to stop user from deselecting a mode const newValue = mode.at(0); if (!newValue) return; - setAppMode(newValue); + setEditorMode(newValue); }; const toggleOffsetMode = (mode: OffsetMode[]) => { @@ -33,7 +36,7 @@ function RundownHeader() { return ( - + } value={AppMode.Run} className={style.button}> Run diff --git a/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx b/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx index faa3daee2..5e3fe4ecf 100644 --- a/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx +++ b/apps/client/src/features/rundown/rundown-header/RundownMenu.tsx @@ -1,11 +1,12 @@ import { memo, useCallback } from 'react'; 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 Dialog from '../../../common/components/dialog/Dialog'; import { useEntryActions } from '../../../common/hooks/useEntryAction'; -import { AppMode, useAppMode } from '../../../common/stores/appModeStore'; +import { AppMode, sessionKeys } from '../../../ontimeConfig'; import { useEventSelection } from '../useEventSelection'; import style from './RundownHeader.module.scss'; @@ -15,7 +16,10 @@ function RundownMenu() { const [isOpen, handlers] = useDisclosure(); 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 deleteAll = useCallback(() => { @@ -26,15 +30,15 @@ function RundownMenu() { return ( <> - + - + -
- - -
+ const [cuesheetMode, setCuesheetMode] = useSessionStorage({ + key: sessionKeys.cuesheetMode, + defaultValue: AppMode.Edit, + }); -
- - - -
-
- ); -} - -function ViewSettingsFollowButton() { - const followPlayback = usePersistedCuesheetOptions((state) => state.followPlayback); - const toggle = usePersistedCuesheetOptions((state) => state.toggleOption); + const toggleCuesheetMode = (mode: AppMode[]) => { + // we need to stop user from deselecting a mode + const newValue = mode.at(0); + if (!newValue) return; + setCuesheetMode(newValue); + }; return ( - + + + + + + } value={AppMode.Run} className={style.radioButton}> + Run + + } value={AppMode.Edit} className={style.radioButton}> + Edit + + + + + + ); } @@ -68,59 +78,60 @@ function ViewSettings() { - Settings - - + + Settings + + + } + /> } /> - - Element visibility - - options.setOption('showActionMenu', checked)} - /> - Show action menu - - - options.setOption('hideTableSeconds', checked)} - /> - Hide seconds in table - - - options.setOption('hidePast', checked)} - /> - Hide past events - - - options.setOption('hideIndexColumn', checked)} - /> - Hide index column - + +
+ Element visibility + + options.setOption('hideTableSeconds', checked)} + /> + Hide seconds in table + + + options.setOption('hidePast', checked)} + /> + Hide past events + + + options.setOption('hideIndexColumn', checked)} + /> + Hide index column + +
- Table Behaviour - - options.setOption('showDelayedTimes', checked)} - /> - Show delayed times - - - options.setOption('hideDelays', checked)} - /> - Hide delay entries - +
+ Table Behaviour + + options.setOption('showDelayedTimes', checked)} + /> + Show delayed times + + + options.setOption('hideDelays', checked)} + /> + Hide delay entries + +
); @@ -136,10 +147,14 @@ function ColumnSettings({ - View - - + + View + + + } + /> } /> diff --git a/apps/client/src/views/cuesheet/cuesheet.options.ts b/apps/client/src/views/cuesheet/cuesheet.options.ts index daa06ea46..feb76eec7 100644 --- a/apps/client/src/views/cuesheet/cuesheet.options.ts +++ b/apps/client/src/views/cuesheet/cuesheet.options.ts @@ -2,9 +2,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; type OptionValues = { - showActionMenu: boolean; hideTableSeconds: boolean; - followPlayback: boolean; hidePast: boolean; hideIndexColumn: boolean; showDelayedTimes: boolean; @@ -12,9 +10,7 @@ type OptionValues = { }; const defaultOptions: OptionValues = { - showActionMenu: false, hideTableSeconds: false, - followPlayback: false, hidePast: false, hideIndexColumn: false, showDelayedTimes: false,