styles: v2 (#229)

* style: revise selected style
* style: revise editor styles
* style: revise viewer styles
* chore: update tests
This commit is contained in:
Carlos Valente
2022-10-21 19:50:25 +02:00
committed by GitHub
parent 08d2ebcc5e
commit d8ee0d4f82
62 changed files with 340 additions and 398 deletions
@@ -26,9 +26,10 @@ export default function ActionButtons(props: ActionButtonProps) {
<MenuButton
as={IconButton}
aria-label='Options'
size='xs'
size='sm'
icon={<FiPlus />}
colorScheme='blue'
variant='ghost'
/>
</Tooltip>
<MenuList style={menuStyle}>
@@ -3,7 +3,7 @@ import { IconButton, Tooltip } from '@chakra-ui/react';
import PropTypes from 'prop-types';
export default function TooltipLoadingActionBtn(props) {
const { clickHandler, icon, color, size = 'xs', tooltip, ...rest } = props;
const { clickHandler, icon, size = 'xs', tooltip, ...rest } = props;
const [loading, setLoading] = useState(false);
const handleClick = useCallback(() => {
@@ -29,7 +29,6 @@ export default function TooltipLoadingActionBtn(props) {
TooltipLoadingActionBtn.propTypes = {
clickHandler: PropTypes.func,
icon: PropTypes.element,
color: PropTypes.string,
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
tooltip: PropTypes.string,
};
@@ -5,10 +5,10 @@ import PropTypes from 'prop-types';
import style from './CollapseBar.module.scss';
export default function CollapseBar(props) {
const { title = 'Collapse bar', isCollapsed, onClick, roll } = props;
const { title = 'Collapse bar', isCollapsed, onClick } = props;
return (
<div className={roll ? style.headerRoll : style.header}>
<div className={style.header}>
{title}
<Icon
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
@@ -22,5 +22,4 @@ CollapseBar.propTypes = {
title: PropTypes.string,
isCollapsed: PropTypes.bool,
onClick: PropTypes.func,
roll: PropTypes.bool,
};
@@ -2,7 +2,6 @@
input[type="color"] {
appearance: none;
background-color: $action-blue;
cursor: pointer;
height: 32px;
width: 32px;
@@ -133,7 +133,7 @@ export default function TimeInput(props) {
size='sm'
icon={<IoLink style={{ transform: 'rotate(-45deg)' }} />}
aria-label='automate'
colorScheme='blue'
colorScheme='whiteAlpha'
style={{ borderRadius: '2px', width: 'min-content' }}
tabIndex={-1}
/>
@@ -2,9 +2,8 @@
.emptyContainer {
width: 100%;
height: 100%;
text-align: center;
color: $bg-black-300;
color: $bg-black-200;
.empty {
width: 100%;
@@ -23,7 +23,7 @@ describe('Message Control input blocks', () => {
// Presenter dialog and button
// substring match, ignore case
render(<MockMessageControl />);
expect(screen.getByPlaceholderText(/presenter/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/stage/i)).toBeInTheDocument();
});
test('Public dialog', async () => {
@@ -40,7 +40,7 @@ export default function InputRow(props: InputRowProps) {
value={inputText}
placeholder={placeholder}
className={style.inline}
color={text === '' ? '#666' : 'inherit'}
color={text === '' ? '#505050' : 'inherit'}
>
<EditablePreview className={`${style.padleft} ${style.fullWidth}`} />
<EditableInput className={style.padleft} />
@@ -17,7 +17,7 @@ export default function MessageControl() {
<div className={style.messageContainer}>
<InputRow
label='Timer screen message'
placeholder='only the presenter screens see this'
placeholder='Shown in stage timer'
text={data.presenter.text}
visible={data.presenter.visible}
changeHandler={(newValue) => setMessage.presenterText(newValue)}
@@ -25,7 +25,7 @@ export default function MessageControl() {
/>
<InputRow
label='Public screen message'
placeholder='public screens will render this'
placeholder='Shown in public screens'
text={data.public.text}
visible={data.public.visible}
changeHandler={(newValue) => setMessage.publicText(newValue)}
@@ -33,7 +33,7 @@ export default function MessageControl() {
/>
<InputRow
label='Lower third message'
placeholder='visible in lower third screen'
placeholder='Shown in lower third'
text={data.lower.text}
visible={data.lower.visible}
changeHandler={(newValue) => setMessage.lowerText(newValue)}
@@ -3,7 +3,6 @@
.messageContainer,
.onAirToggle {
@include main-container;
display: flex;
gap: 0.5em;
padding: 0.5em;
@@ -47,7 +46,6 @@
}
.inline {
background-color: $light-bg-transparent;
border-color: $action-blue;
}
}
@@ -10,8 +10,7 @@ import style from '../../editors/Editor.module.scss';
export default function MessageControlExport() {
return (
<Box className={style.messages}>
<h1>Messages Control</h1>
<Box className={style.messages} data-testid="panel-messages-control">
<FiArrowUpRight className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
<div className={style.content}>
<ErrorBoundary>
@@ -8,9 +8,7 @@
gap: 4px;
}
.timeContainer,
.playbackContainer {
@include second-container;
padding: 0.5em;
}
@@ -10,8 +10,7 @@ import style from '../../editors/Editor.module.scss';
export default function TimerControlExport() {
return (
<Box className={style.playback}>
<h1>Timer Control</h1>
<Box className={style.playback} data-testid="panel-timer-control">
<FiArrowUpRight className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
<div className={style.content}>
<ErrorBoundary>
@@ -1,4 +1,4 @@
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
import { IoPlayBack } from '@react-icons/all-files/io5/IoPlayBack';
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import PropTypes from 'prop-types';
@@ -30,7 +30,7 @@ export default function Transport(props) {
clickHandler={() => playbackControl.reload()}
disabled={selectedId == null || isRolling || noEvents}
tooltip='Reload event'
icon={<IoArrowUndo size='22px' />}
icon={<IoPlayBack size='22px' />}
/>
<UnloadIconBtn
clickHandler={() => playbackControl.stop()}
+10 -18
View File
@@ -114,12 +114,6 @@
}
}
h1 {
font-size: max(1.5em, 16px);
color: $bg-gray-100;
padding-bottom: 0.25em;
}
.mainContainer {
.settings,
.editor,
@@ -128,9 +122,8 @@ h1 {
.info {
border-radius: 8px;
height: 100%;
background-color: $bg-gray-1000;
background-color: $bg-container-l1;
padding: 0.8em 1.5em;
display: flex;
flex-direction: column;
}
@@ -138,9 +131,8 @@ h1 {
.eventEditor {
border-radius: 3px 3px 0 0;
background-color: $bg-gray-950;
border-top: 1px solid $bg-gray-900;
padding: 0.8em 1.5em;
background-color: $bg-container-l1;
border-top: $border-l1;
position: absolute;
bottom: 0;
width: 100vw;
@@ -155,14 +147,14 @@ h1 {
transition: bottom 0.7s;
}
.header {
.eventEditorLayout {
display: flex;
}
.header {
h1 {
margin-right: auto;
}
background-color: $bg-container-l2;
padding: 8px;
border-left: 1px solid $bg-container-l3;
}
}
@@ -194,8 +186,8 @@ h1 {
.playback {
grid-area: play;
min-height: 320px;
max-height: 320px;
min-height: 270px;
max-height: 270px;
min-width: 480px;
}
@@ -211,5 +203,5 @@ h1 {
}
.content {
padding-top: 0.5em;
padding-top: 24px;
}
+1 -1
View File
@@ -39,7 +39,7 @@ export default function Editor() {
<ErrorBoundary>
<ModalManager isOpen={isSettingsOpen} onClose={onSettingsClose} />
</ErrorBoundary>
<div className={styles.mainContainer}>
<div className={styles.mainContainer} data-testid="event-editor">
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar
@@ -23,9 +23,11 @@ export default function BlockBlock(props) {
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
icon={<IoRemove />}
colorScheme='red'
tooltip='Delete'
_hover={{ bg: 'red.400' }}
variant='ghost'
_hover={{ bg: 'red.400', color: 'white' }}
color='red.500'
size='sm'
/>
<ActionButtons showAdd showDelay actionHandler={actionHandler} />
</HStack>
@@ -54,7 +54,7 @@ export default function DelayBlock(props) {
<HStack spacing='4px' className={style.actionOverlay}>
<Button
onClick={applyDelayHandler}
size='xs'
size='sm'
colorScheme='orange'
_hover={{ bg: 'orange.400' }}
leftIcon={<FiCheck />}
@@ -64,9 +64,11 @@ export default function DelayBlock(props) {
<TooltipLoadingActionBtn
clickHandler={deleteHandler}
icon={<IoRemove />}
colorScheme='red'
tooltip='Delete'
_hover={{ bg: 'red.400' }}
variant='ghost'
_hover={{ bg: 'red.400', color: 'white' }}
color='red.500'
size='sm'
/>
<ActionButtons showAdd actionHandler={actionHandler} />
</HStack>
@@ -11,7 +11,6 @@
}
.create {
background-color: rgba($bg-gray-1100, 0.85);
box-sizing: border-box;
width: 100%;
@@ -5,13 +5,12 @@
// layout
display: grid;
grid-template-areas:
"binder progb progb progb"
"binder pb-actions times actions"
"binder pb-actions title title"
"binder pb-actions notes status"
"binder ... ... ...";
"binder progb progb progb";
grid-template-columns: $binder-width auto 1fr auto;
grid-template-rows: $element-spacing 36px 36px 36px $element-spacing;
grid-template-rows: 36px 36px 36px $element-spacing;
align-items: center;
margin: 2px 1px;
padding-right: $clearance;
@@ -21,16 +20,13 @@
gap: 2px;
// style - colour
background-color: $bg-gray-950;
border: $block-border;
// style - text
background-color: $bg-container-l2;
border: $border-l3;
// variant
&.selected {
background-color: $bg-gray-900;
border: 1px solid $ontime-accent;
background-color: $bg-container-l3;
box-shadow: 0 0 1px 2px $ontime-accent;
}
&.skip {
@@ -39,8 +35,7 @@
.eventTimers > .delayNote,
.eventTitle,
.eventNote,
{
.eventNote {
color: $text-gray-disabled;
}
@@ -65,11 +60,9 @@
border-right: 1px solid $bg-gray-900;
// style - colour
background-color: $bg-gray-950; // override inline
background-color: $bg-container-l3; // override inline
color: $text-white;
// style - text
.drag {
@include drag-style;
position: absolute;
@@ -184,7 +177,9 @@
.progressBg {
grid-area: progb;
border-radius: 1px;
background-color: $bg-black-100;
background-color: $bg-container-l1;
margin-bottom: 4px;
margin-left: 4px;
// layout
height: 100%;
@@ -194,8 +189,6 @@
height: 100%;
width: 0;
border-radius: 1px 0 0 1px;
margin-top: 2px;
margin-left: 4px;
// animations
transition: 1s linear;
@@ -1,14 +1,16 @@
import { useCallback, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { Editable, EditableInput, EditablePreview, IconButton, Tooltip } from '@chakra-ui/react';
import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/react';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoPlayBackOutline } from '@react-icons/all-files/io5/IoPlayBackOutline';
import { IoPlayOutline } from '@react-icons/all-files/io5/IoPlayOutline';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircleSharp } from '@react-icons/all-files/io5/IoRemoveCircleSharp';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { IoReturnDownForward } from '@react-icons/all-files/io5/IoReturnDownForward';
import { IoSettingsSharp } from '@react-icons/all-files/io5/IoSettingsSharp';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { editorEventId } from 'common/atoms/LocalEventSettings';
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
@@ -27,9 +29,8 @@ import style from './EventBlock.module.scss';
const blockBtnStyle = {
size: 'sm',
colorScheme: 'blue',
variant: 'outline',
borderRadius: '3px',
colorScheme: 'white',
variant: 'ghost',
fontSize: '20px',
};
@@ -103,6 +104,12 @@ export default function EventBlock(props: EventBlockProps) {
// Todo: data should come from socket
const progress = `${Math.random() * 100}%`;
const eventIsPlaying = selected && playback === 'start';
const playBtnStyles = { _hover: {} };
if (!skip && eventIsPlaying) {
playBtnStyles._hover = { bg: '#c05621' };
} else if (!skip && !eventIsPlaying) {
playBtnStyles._hover = { bg: '#4bffab' };
}
return (
<Draggable key={eventId} draggableId={eventId} index={index}>
@@ -136,37 +143,37 @@ export default function EventBlock(props: EventBlockProps) {
aria-label='Skip event'
tooltip='Skip event'
openDelay={tooltipDelayMid}
icon={<IoRemoveCircleSharp />}
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
{...blockBtnStyle}
variant={skip ? 'solid' : 'outline'}
variant={skip ? 'solid' : 'ghost'}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
tabIndex={-1}
disabled={selected}
/>
<TooltipActionBtn
aria-label='Start event'
tooltip='Start event'
aria-label='Load event'
tooltip='Load event'
openDelay={tooltipDelayMid}
icon={eventIsPlaying ? <IoPause /> : <IoPlay />}
icon={selected ? <IoPlayBackOutline /> : <IoReload />}
disabled={skip}
{...blockBtnStyle}
variant={eventIsPlaying ? 'solid' : 'outline'}
clickHandler={() => setPlayback.loadEvent()}
tabIndex={-1}
/>
<TooltipActionBtn
aria-label='Toggle start / pause event'
tooltip={eventIsPlaying ? 'Pause event' : 'Start event'}
openDelay={tooltipDelayMid}
icon={eventIsPlaying ? <IoPause /> : <IoPlayOutline />}
disabled={skip}
{...blockBtnStyle}
variant={eventIsPlaying ? 'solid' : 'ghost'}
clickHandler={
eventIsPlaying
? () => setPlayback.pause()
: () => setPlayback.startEvent()
}
tabIndex={-1}
/>
<TooltipActionBtn
aria-label='Load event'
tooltip='Load event'
openDelay={tooltipDelayMid}
icon={selected ? <IoReload /> : <IoReload />}
disabled={skip}
{...blockBtnStyle}
variant={selected ? 'solid' : 'outline'}
clickHandler={() => setPlayback.loadEvent()}
{...playBtnStyles}
tabIndex={-1}
/>
</div>
@@ -190,13 +197,17 @@ export default function EventBlock(props: EventBlockProps) {
</Editable>
<span className={style.eventNote}>{note}</span>
<div className={style.eventActions}>
<IconButton
icon={<IoSettingsSharp />}
aria-label='event options'
onClick={() => setOpenId((prev) => prev === eventId ? null : eventId)}
<TooltipActionBtn
{...blockBtnStyle}
variant={openId === eventId ? 'solid' : 'outline'}
size='sm'
icon={<IoOptions />}
clickHandler={() => setOpenId((prev) => prev === eventId ? null : eventId)}
tooltip='Event options'
aria-label='Event options'
tabIndex={-1}
backgroundColor={openId === eventId ? '#ebedf0' : 'transparent'}
color={openId === eventId ? '#333' : '#ebedf0'}
_hover={{ bg: '#ebedf0', color: '#333' }}
/>
<EventBlockActionMenu
showAdd
@@ -27,9 +27,8 @@ export default function EventBlockActionMenu(props) {
const blockBtnStyle = {
size: 'sm',
variant: 'ghost',
colorScheme: 'blue',
variant: 'outline',
borderRadius: '3px',
};
return (
@@ -1,5 +1,6 @@
import { createRef, useCallback, useContext, useEffect } from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Button } from '@chakra-ui/react';
import {
defaultPublicAtom,
showQuickEntryAtom,
@@ -166,7 +167,12 @@ export default function EventList(props) {
);
if (events.length < 1) {
return <Empty text='No Events' style={{ marginTop: '10vh' }} />;
return (
<div className={style.alignCenter}>
<Empty text='No Events' style={{ marginTop: '7vh' }} />
<Button variant='solid' colorScheme='blue'>Create Event</Button>
</div>
);
}
let cumulativeDelay = 0;
let eventIndex = -1;
@@ -11,8 +11,7 @@ import style from '../Editor.module.scss';
export default function EventListExport() {
return (
<CursorProvider>
<Box className={style.editor}>
<h1>Event List</h1>
<Box className={style.editor} data-testid="panel-event-list">
<FiArrowUpRight
className={style.corner}
onClick={(event) => handleLinks(event, 'eventlist')}
@@ -20,15 +20,13 @@ export default function EventListWrapper() {
}, [emitError, isError]);
return (
<>
<div className={styles.content}>
<EventListMenu />
<div className={styles.content}>
{status === 'success' && data ? (
<EventList events={data} />
) : (
<Empty text='Connecting to server' />
)}
</div>
</>
{status === 'success' && data ? (
<EventList events={data} />
) : (
<Empty text='Connecting to server' />
)}
</div>
);
}
@@ -2,7 +2,6 @@
@use '../../../theme/mixins' as *;
.eventContainer {
@include second-container;
margin-top: 1em;
display: flex;
flex-direction: column;
@@ -22,8 +21,8 @@
}
.cursor {
box-shadow: 2px 2px 0 $ontime-pink;
border-radius: 3px;
outline: 1px solid $action-blue;
}
.bgElement {
@@ -31,3 +30,11 @@
background: rgba(214, 158, 46, 0.3);
}
}
.alignCenter {
text-align: center;
flex-direction: column;
button {
margin-top: 24px;
}
}
@@ -162,7 +162,7 @@ export default function EventEditor() {
leftIcon={<FiUsers />}
size='sm'
colorScheme='blue'
variant={event.isPublic ? 'solid' : 'outline'}
variant={event.isPublic ? 'solid' : 'ghost'}
onClick={() => togglePublic(event.isPublic)}
>
{event.isPublic ? 'Event is Public' : 'Make event public'}
@@ -180,7 +180,7 @@ export default function EventEditor() {
<Button
leftIcon={<IoBan />}
onClick={() => handleSubmit('colour', '')}
variant='outline'
variant='ghost'
colorScheme='blue'
borderRadius='3px'
size='sm'
@@ -1,8 +1,10 @@
@use '../../theme/main' as *;
.eventEditor {
padding: 16px 32px;
width: 100%;
overflow-y: scroll;
max-height: 33vh;
max-height: 40vh;
gap: max(16px, 2vh);
display: grid;
@@ -11,7 +13,7 @@
.timers,
.titles {
background-color: $bg-gray-1100;
background-color: $bg-container-l2;
border-radius: 2px;
padding: 8px 16px;
}
@@ -22,6 +24,9 @@
flex-direction: column;
gap: 8px;
}
.timers label:nth-child(1) {
margin-top: 4px;
}
.titles {
grid-area: titles;
@@ -36,6 +41,7 @@
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.left {
@@ -46,8 +52,7 @@
.right {
padding-left: 16px;
grid-area: right;
border-left: 1px solid $bg-gray-900;
border-left: 1px solid $bg-container-l3;
}
.osc {
@@ -22,17 +22,18 @@ export default function InfoExport() {
return (
<Box className={`${style.eventEditor} ${!openId ? style.noEvent : ''}`}>
<ErrorBoundary>
<div className={style.header}>
<h1>Event Editor</h1>
<IconButton
aria-label='Close Menu'
icon={<FiX />}
onClick={() => setOpenId(null)}
{...closeBtnStyle}
/>
</div>
<div className={style.content}>
<div className={style.eventEditorLayout}>
<EventEditor />
<div className={style.header}>
<IconButton
aria-label='Close Menu'
size='lg'
icon={<FiX />}
onClick={() => setOpenId(null)}
{...closeBtnStyle}
/>
</div>
</div>
</ErrorBoundary>
</Box>
+2 -2
View File
@@ -36,8 +36,8 @@ export default function Info() {
<span>{selected}</span>
</div>
<InfoNif />
<InfoTitle title='Now' data={titlesNow} roll={data.playback === 'roll'} />
<InfoTitle title='Next' data={titlesNext} roll={data.playback === 'roll'} />
<InfoTitle title='Now' data={titlesNow} />
<InfoTitle title='Next' data={titlesNext} />
<InfoLogger />
</>
);
+17 -11
View File
@@ -2,15 +2,17 @@
@use '../../theme/mixins' as *;
@mixin container {
margin-top: 1em;
@include second-container;
margin-top: 1em;
padding: 8px;
}
.container {
@include container;
border: 1px solid transparent;
}
.main {
font-size: 0.9em;
color: $ontime-pink;
@@ -32,10 +34,6 @@
color: $header-gray;
}
.headerRoll {
color: $ontime-roll;
}
.collapsedTitle {
font-size: inherit;
@@ -78,10 +76,18 @@
text-overflow: ellipsis;
}
.if {
font-size: 0.8em;
color: $ontime-accent;
background-color: $bg-gray-900;
padding: 0 0.5em;
margin: 0 0.5em;
.interfaceList {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.interface {
font-size: 0.8em;
background-color: $bg-container-l3;
padding: 0 8px;
margin: 0 2px;
border-radius: 2px;
white-space: nowrap;
}
+1 -2
View File
@@ -10,8 +10,7 @@ import style from '../editors/Editor.module.scss';
export default function InfoExport() {
return (
<Box className={style.info}>
<h1>Info</h1>
<Box className={style.info} data-testid="panel-info">
<FiArrowUpRight className={style.corner} onClick={(event) => handleLinks(event, 'info')} />
<div className={style.content}>
<ErrorBoundary>
@@ -22,7 +22,6 @@
padding: 0 0.5em;
margin: 0 0.5em;
li {
display: flex;
margin-bottom: 2px;
+2 -2
View File
@@ -19,13 +19,13 @@ export default function InfoNif() {
onClick={() => setCollapsed((c) => !c)}
/>
{!collapsed && (status === 'success') &&(
<div>
<div className={style.interfaceList}>
{data?.networkInterfaces.map((e) => (
<a
key={e.address}
href='#!'
onClick={() => openLink(baseURL.replace('__IP__', e.address))}
className={style.if}
className={style.interface}
>
{`${e.name} - ${e.address}`}
</a>
+1 -3
View File
@@ -6,10 +6,9 @@ import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import style from './Info.module.scss';
export default function InfoTitle(props) {
const { title, data } = props;
const [collapsed, setCollapsed] = useState(false);
const { title, data, roll } = props;
const noTitl = data.title == null || data.title === '';
const noPres = data.presenter == null || data.presenter === '';
const noSubt = data.subtitle == null || data.subtitle === '';
@@ -21,7 +20,6 @@ export default function InfoTitle(props) {
title={title}
isCollapsed={collapsed}
onClick={() => setCollapsed((c) => !c)}
roll={roll}
/>
{!collapsed && (
<>
@@ -1,89 +0,0 @@
import { memo, useCallback, useContext } from 'react';
import { ButtonGroup, HStack } from '@chakra-ui/react';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
import { CursorContext } from 'common/context/CursorContext';
import { useEventAction } from 'common/hooks/useEventAction';
import MenuActionButtons from './MenuActionButtons';
import style from './EventListMenu.module.css';
const cursorBtnProps = {
size: 'sm',
color: 'pink.300',
borderColor: 'pink.300',
variant: 'outline',
_hover: { bg: 'pink.400', color: 'white' },
};
const EventListMenu = () => {
const { isCursorLocked, toggleCursorLocked, moveCursorUp, moveCursorDown } =
useContext(CursorContext);
const { addEvent, deleteAllEvents } = useEventAction();
const actionHandler = useCallback(
(action) => {
switch (action) {
case 'event':
addEvent({ type: action });
break;
case 'delay':
addEvent({ type: action });
break;
case 'block':
addEvent({ type: action });
break;
case 'cursorUp':
moveCursorUp();
break;
case 'cursorDown':
moveCursorDown();
break;
case 'togglelock':
toggleCursorLocked();
break;
case 'deleteall':
deleteAllEvents();
break;
default:
break;
}
},
[addEvent, deleteAllEvents, moveCursorDown, moveCursorUp, toggleCursorLocked],
);
return (
<HStack className={style.headerButtons}>
<ButtonGroup isAttached>
<TooltipActionBtn
{...cursorBtnProps}
clickHandler={() => actionHandler('cursorUp')}
icon={<IoCaretUp />}
tooltip='Move cursor up Alt + ↑'
/>
<TooltipActionBtn
{...cursorBtnProps}
clickHandler={() => actionHandler('cursorDown')}
icon={<IoCaretDown />}
tooltip='Move cursor down Alt + ↓'
/>
<TooltipActionBtn
{...cursorBtnProps}
clickHandler={() => actionHandler('togglelock')}
icon={<FiTarget />}
tooltip='Lock cursor to current'
width='3em'
backgroundColor={isCursorLocked && 'pink.400'}
color={isCursorLocked ? 'white' : 'pink.400'}
variant={isCursorLocked ? 'solid' : 'outline'}
/>
</ButtonGroup>
<MenuActionButtons actionHandler={actionHandler} size='sm' />
</HStack>
);
};
export default memo(EventListMenu);
@@ -1,5 +1,5 @@
.headerButtons {
align-content: center;
justify-content: flex-end;
justify-content: space-between;
gap: 8px;
}
@@ -0,0 +1,97 @@
import { memo, useCallback, useContext } from 'react';
import {
Button,
Divider,
HStack,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Tooltip,
} from '@chakra-ui/react';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
import { CursorContext } from 'common/context/CursorContext';
import { useEventAction } from '../../common/hooks/useEventAction';
import style from './EventListMenu.module.scss';
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
const EventListMenu = () => {
const { isCursorLocked, toggleCursorLocked } = useContext(CursorContext);
const { addEvent, deleteAllEvents } = useEventAction();
type ActionTypes = 'event' | 'delay' | 'block' | 'delete-all' | 'toggle-lock';
const actionHandler = useCallback(
(action: ActionTypes) => {
switch (action) {
case 'toggle-lock':
toggleCursorLocked();
break;
case 'event':
addEvent({ type: action });
break;
case 'delay':
addEvent({ type: action });
break;
case 'block':
addEvent({ type: action });
break;
case 'delete-all':
deleteAllEvents();
break;
}
},
[toggleCursorLocked],
);
return (
<HStack className={style.headerButtons}>
<Button
size='sm'
variant={isCursorLocked ? 'solid' : 'ghost'}
onClick={() => actionHandler('toggle-lock')}
colorScheme='blue'
>
Lock cursor to current
</Button>
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add / Delete ...'>
<MenuButton
as={IconButton}
aria-label='Create Menu'
size='sm'
icon={<FiPlus />}
colorScheme='blue'
variant='outline'
/>
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')}>
Add Event first
</MenuItem>
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')}>
Add Delay first
</MenuItem>
<MenuItem icon={<FiMinusCircle />} onClick={() => actionHandler('block')}>
Add Block first
</MenuItem>
<Divider />
<MenuItem icon={<FiTrash2 />} onClick={() => actionHandler('delete-all')} color='red.500'>
Delete All
</MenuItem>
</MenuList>
</Menu>
</HStack>
);
};
export default memo(EventListMenu);
@@ -1,61 +0,0 @@
import {
IconButton,
Divider,
Menu,
MenuButton,
MenuItem,
MenuList,
Tooltip,
} from '@chakra-ui/react';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
import { useEventAction } from 'common/hooks/useEventAction';
import { Size } from 'common/models/UtilTypes';
interface MenuActionButtonsProps {
actionHandler: (action: string) => void;
size?: Size;
}
// Todo: add useEventActionsHook
export default function MenuActionButtons(props: MenuActionButtonsProps) {
const { actionHandler, size = 'xs' } = props;
const { deleteAllEvents } = useEventAction();
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add / Delete ...'>
<MenuButton
as={IconButton}
aria-label='Create Menu'
size={size}
icon={<FiPlus />}
colorScheme='blue'
/>
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')}>
Add Event first
</MenuItem>
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')}>
Add Delay first
</MenuItem>
<MenuItem icon={<FiMinusCircle />} onClick={() => actionHandler('block')}>
Add Block first
</MenuItem>
<Divider />
<MenuItem icon={<FiTrash2 />} onClick={() => deleteAllEvents()} color='red.500'>
Delete All
</MenuItem>
</MenuList>
</Menu>
);
}
+2 -2
View File
@@ -4,8 +4,8 @@ import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
import { FiSave } from '@react-icons/all-files/fi/FiSave';
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadEvents } from 'common/api/ontimeApi';
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
@@ -116,7 +116,7 @@ export default function MenuBar(props: MenuBarProps) {
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiSettings />}
icon={<IoSettingsOutline />}
className={isSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
@@ -1,26 +0,0 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { vi } from 'vitest';
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
import MenuActionButtons from '../MenuActionButtons';
const actionHandler = vi.fn();
const renderInMock = () => {
render(
<QueryClientProvider client={queryClientMock}>
<MenuActionButtons actionHandler={actionHandler} />
</QueryClientProvider>,
);
};
test('check that menu bar renders correctly', () => {
// need to inject the react query provider
renderInMock();
const b = screen.getByRole('button', {
name: /create menu/i,
});
expect(b).toBeInTheDocument();
});
+4 -1
View File
@@ -83,7 +83,10 @@ export default function TableWrapper() {
return <span>loading...</span>;
}
return (
<div className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}>
<div
className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}
data-testid="cuesheet"
>
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
<OntimeTable
tableData={events}
@@ -58,7 +58,7 @@ export default function Backstage(props) {
const clock = formatTime(time.clock, formatOptions);
return (
<div className='backstage'>
<div className='backstage' data-testid="backstage-view">
<NavLogo />
<div className='event-title'>{general.title}</div>
@@ -31,7 +31,7 @@
.today-container {
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
padding: 1vh 1vw;
border-radius: 1vw;
border-radius: $element-border-radius;
.label {
font-size: 1.3vw;
@@ -50,7 +50,7 @@
}
.event {
border-radius: 0 2vw 2vw 0;
border-radius: 0 $element-border-radius $element-border-radius 0;
margin-left: -1vw;
padding: 1vh 2vw;
overflow: hidden;
@@ -115,12 +115,12 @@
.clock-container {
grid-area: time;
border-radius: 0 0 1vw 1vw;
border-radius: 0 0 $element-border-radius $element-border-radius;
}
.timer-container {
grid-area: clck;
border-radius: 1vw 1vw 0 0;
border-radius: $element-border-radius $element-border-radius 0 0;
}
.info {
@@ -90,10 +90,10 @@ export default function Countdown(props) {
: formatTime(follow.timeEnd + delay, formatOptions);
return (
<div className='countdown'>
<div className='countdown' data-testid="countdown-view">
<NavLogo />
{follow === null ? (
<div className='event-select'>
<div className='event-select' data-testid="countdown-select">
<span className='event-select__title'>Select an event to follow</span>
<ul className='event-select__events'>
{backstageEvents.length === 0 ? (
@@ -112,7 +112,7 @@ export default function Countdown(props) {
</ul>
</div>
) : (
<div className='countdown-container'>
<div className='countdown-container' data-testid="countdown-event">
<div className='timer-group'>
<div className='aux-timers'>
<div className='aux-timers__label'>Time Now</div>
@@ -80,7 +80,7 @@
color: var(--accent-color-override, $accent-color);
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
padding: 1vh 2vw;
border-radius: 1vw;
border-radius: $element-border-radius;
}
.status {
@@ -10,7 +10,7 @@
.lower-container {
position: absolute;
padding: 1vh 2vh;
border-radius: 1vh;
border-radius: 8px;
}
.message-container {
@@ -15,7 +15,7 @@
display: flex;
flex-direction: column;
width: 45vw;
border-radius: 0 1vh 1vh 0;
border-radius: 0 8px 8px 0;
}
.message-container {
@@ -65,7 +65,7 @@ export default function Pip(props) {
const clock = formatTime(time.clock, formatOptions);
return (
<div className='pip'>
<div className='pip' data-testid="pip-view">
<NavLogo />
<div className='event-title'>{general.title}</div>
@@ -27,7 +27,7 @@
.timer-container {
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
padding: 1vh 1vw;
border-radius: 1vw;
border-radius: $element-border-radius;
.label {
font-size: 1.3vw;
@@ -126,11 +126,11 @@
.clock-container {
grid-area: time;
border-radius: 0 0 1vw 1vw;
border-radius: 0 0 $element-border-radius $element-border-radius;
}
.timer-container {
grid-area: clck;
border-radius: 1vw 1vw 0 0;
border-radius: $element-border-radius $element-border-radius 0 0;
}
}
@@ -38,7 +38,7 @@ export default function Public(props) {
const clock = formatTime(time.clock, formatOptions);
return (
<div className='public-screen'>
<div className='public-screen' data-testid="public-view">
<NavLogo />
<div className='event-title'>{general.title}</div>
@@ -31,7 +31,7 @@
.today-container {
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
padding: 1vh 1vw;
border-radius: 1vw;
border-radius: $element-border-radius;
.label {
font-size: 1.3vw;
@@ -50,7 +50,7 @@
}
.event {
border-radius: 0 2vw 2vw 0;
border-radius: 0 $element-border-radius $element-border-radius 0;
margin-left: -1vw;
padding: 1vh 2vw;
overflow: hidden;
@@ -114,7 +114,7 @@
.clock-container {
grid-area: time;
border-radius: 1vw;
border-radius: $element-border-radius;
}
.info {
@@ -54,7 +54,7 @@ export default function StudioClock(props) {
const [, , secondsNow] = stringFromMillis(time.clock).split(':');
return (
<div className='studio-clock'>
<div className='studio-clock' data-testid="studio-view">
<NavLogo />
<div className='clock-container'>
<div className='studio-timer'>{clock}</div>
+4 -1
View File
@@ -66,7 +66,10 @@ export default function Timer(props) {
};
return (
<div className={time.finished ? 'stage-timer stage-timer--finished' : 'stage-timer'}>
<div
className={time.finished ? 'stage-timer stage-timer--finished' : 'stage-timer'}
data-testid="timer-view"
>
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
<div className='message'>{pres.text}</div>
</div>
+1 -1
View File
@@ -30,7 +30,7 @@
.event {
background-color: var(--outdent-background-color-override, $viewer-outdent-bg-color);
padding: 1vh 2vw;
border-radius: 1vw;
border-radius: $element-border-radius;
max-width: 100%;
&.now {
+1 -1
View File
@@ -3,7 +3,7 @@
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.01);
scrollbar-color: #2b2b2b rgba(255, 255, 255, 0.01);
scrollbar-width: thin;
}
+9 -2
View File
@@ -6,6 +6,7 @@ $bg-black-100: #070707;
$bg-black-200: #1a1a1a; // container borders
$bg-black-300: #1f1f1f; // container text
$bg-gray-1100: #232323; // container borders
$bg-gray-1050: #242424; // container borders
$bg-gray-1000: #262626; // container borders
$bg-gray-950: #292929;
$bg-gray-900: #303030;
@@ -42,7 +43,7 @@ $opacity-disabled: 0.4;
$notes-color: #d69e2e;
$text-white: #fffffa;
$text-gray-disabled: #999;
$text-gray-disabled: #505050;
$header-gray: #ccc;
$label-gray: #aaa;
$clocks: #ddd;
@@ -64,11 +65,17 @@ $title-gray: #ddd;
$subtitle-gray: #aaa;
//////////////////////////////////// block elements
$bg-container-l1: #202020;
$bg-container-l2: #232323;
$bg-container-l3: #2b2b2b;
$border-l1: 1px solid $bg-gray-1000;
$border-l3: 1px solid $bg-gray-900;
$block-delay-color: #ecc94b;
$delay-text: #d69e2e;
$block-delay-border: #d69e2e55;
$block-block-color: #805ad5;
$block-border: 1px solid $bg-gray-900;
$block-border: 1px solid $bg-gray-1100;
//////////////////////////////////// viewer cards
@mixin card-title {
+2 -3
View File
@@ -9,13 +9,12 @@
}
@mixin second-container {
background-color: $bg-gray-1100;
border: 1px solid rgba(0, 0, 0, 0.05);
background-color: $bg-container-l2;
border-radius: 4px;
}
@mixin third-container {
background-color: $bg-black-300;
background-color: $bg-gray-1100;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 2px;
}
+2 -1
View File
@@ -7,8 +7,9 @@ $delay-color: $delay-text;
// Main Properties of a viewer
$viewer-background-color: $bg-black; // --background-color-override
$viewer-color: $title-white; // --color-override
$viewer-outdent-bg-color: $bg-gray-1100; // --outdent-background-color-override
$viewer-outdent-bg-color: $bg-container-l1; // --outdent-background-color-override
$viewer-overlay-bg-color: $bg-overlay;
$element-border-radius: 8px;
// Properties related to timer
$timer-finished-color: $ontime-pink-variant;
+40 -39
View File
@@ -3,38 +3,37 @@ describe('validate routes', () => {
describe('viewer routes', () => {
it('default to stage timer', () => {
cy.visit('http://localhost:4001/');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
});
it('renders stage timer', () => {
it('renders stage timer routes', () => {
cy.visit('http://localhost:4001/timer');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
});
it('renders presenter', () => {
cy.visit('http://localhost:4001/presenter');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
});
it('renders speaker', () => {
cy.visit('http://localhost:4001/speaker');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
});
it('renders backstage view /stage', () => {
cy.visit('http://localhost:4001/stage');
cy.get('[data-testid="timer-view"]').should('exist');
cy.contains('Time Now');
});
it('renders backstage view /backstage', () => {
it('renders backstage routes', () => {
cy.visit('http://localhost:4001/backstage');
cy.get('[data-testid="backstage-view"]').should('exist');
cy.contains('Today');
cy.contains('Time Now');
cy.contains('Info');
});
it('renders backstage view /sm', () => {
cy.visit('http://localhost:4001/sm');
cy.get('[data-testid="backstage-view"]').should('exist');
cy.contains('Today');
cy.contains('Time Now');
cy.contains('Info');
@@ -42,6 +41,7 @@ describe('validate routes', () => {
it('renders public view', () => {
cy.visit('http://localhost:4001/public');
cy.get('[data-testid="public-view"]').should('exist');
cy.contains('Today');
cy.contains('Time Now');
cy.contains('Info');
@@ -49,8 +49,8 @@ describe('validate routes', () => {
it('renders pip view', () => {
cy.visit('http://localhost:4001/pip');
cy.get('[data-testid="pip-view"]').should('exist');
cy.contains('Today');
cy.contains('Info');
});
it('renders lower third with no errors', () => {
@@ -60,16 +60,21 @@ describe('validate routes', () => {
it('renders studio clock', () => {
cy.visit('http://localhost:4001/studio');
cy.get('[data-testid="studio-view"]').should('exist');
cy.contains('ON AIR');
});
it('renders countdown selection', () => {
cy.visit('http://localhost:4001/countdown');
cy.get('[data-testid="countdown-view"]').should('exist');
cy.get('[data-testid="countdown-select"]').should('exist');
cy.contains('Select an event to follow');
});
it('renders countdown with event', () => {
cy.visit('http://localhost:4001/countdown?event=1');
cy.get('[data-testid="countdown-view"]').should('exist');
cy.get('[data-testid="countdown-event"]').should('exist');
cy.contains('Time Now');
cy.contains('Start Time');
cy.contains('End Time');
@@ -78,38 +83,30 @@ describe('validate routes', () => {
describe('clock view timer', () => {
it('renders base route', () => {
// base route
cy.visit('http://localhost:4001/clock');
cy.get('[data-testid="clock-view"]').should('exist');
// ontime fallsback to stage timer on errors, so we check for that
cy.get('.App').should('not.contain', 'Time Now');
});
});
describe('minimal timer and options', () => {
it('renders base route', () => {
// base route
cy.visit('http://localhost:4001/minimal');
cy.get('[data-testid="minimal-timer"]').should('exist');
// ontime fallsback to stage timer on errors, so we check for that
cy.get('.App').should('not.contain', 'Time Now');
});
it('renders with defined options', () => {
// route with options
cy.visit(
'http://localhost:4001/minimal?font=arial=1&size=1.5&text=0f0&key=ff0&hideovertime=true&alignx=start&aligny=end&offsetx=100&offsety=-100&hidemessages=true'
);
cy.get('[data-testid="minimal-timer"]').should('exist');
// ontime fallback to stage timer on errors, so we check for that
cy.get('.App').should('not.contain', 'Time Now');
});
it('hides nav on given option', () => {
// route with options for no nav
cy.visit('http://localhost:4001/minimal?hidenav=true');
cy.get('[data-testid="minimal-timer"]').should('exist');
// ontime fallback to stage timer on errors, so we check for that
cy.get('.App').should('not.contain', 'Time Now');
cy.get('[data-testid="nav-logo"]').should('not.exist');
});
@@ -118,49 +115,53 @@ describe('validate routes', () => {
describe('editor routes', () => {
it('app editor', () => {
cy.visit('http://localhost:4001/editor');
cy.contains('Event List');
cy.contains('Timer Control');
cy.contains('Messages Control');
cy.contains('Info');
cy.get('[data-testid="event-editor"]').should('exist');
cy.get('[data-testid="panel-event-list"]').should('exist');
cy.get('[data-testid="panel-timer-control"]').should('exist');
cy.get('[data-testid="panel-messages-control"]').should('exist');
cy.get('[data-testid="panel-info"]').should('exist');
});
it('self contained eventlist', () => {
cy.visit('http://localhost:4001/eventlist');
cy.get('.App').should('contain', 'Event List');
cy.get('.App').should('not.contain', 'Timer Control');
cy.get('.App').should('not.contain', 'Messages Control');
cy.get('.App').should('not.contain', 'Info');
cy.get('[data-testid="panel-event-list"]').should('exist');
cy.get('[data-testid="panel-timer-control"]').should('not.exist');
cy.get('[data-testid="panel-messages-control"]').should('not.exist');
cy.get('[data-testid="panel-info"]').should('not.exist');
});
it('self contained timercontrol', () => {
cy.visit('http://localhost:4001/timercontrol');
cy.get('.App').should('not.contain', 'Event List');
cy.get('.App').should('contain', 'Timer Control');
cy.get('.App').should('not.contain', 'Messages Control');
cy.get('.App').should('not.contain', 'Info');
cy.get('[data-testid="panel-event-list"]').should('not.exist');
cy.get('[data-testid="panel-timer-control"]').should('exist');
cy.get('[data-testid="panel-messages-control"]').should('not.exist');
cy.get('[data-testid="panel-info"]').should('not.exist');
});
it('self contained messagecontrol', () => {
cy.visit('http://localhost:4001/messagecontrol');
cy.get('.App').should('not.contain', 'Event List');
cy.get('.App').should('not.contain', 'Timer Control');
cy.get('.App').should('contain', 'Messages Control');
cy.get('.App').should('not.contain', 'Info');
cy.get('[data-testid="panel-event-list"]').should('not.exist');
cy.get('[data-testid="panel-timer-control"]').should('not.exist');
cy.get('[data-testid="panel-messages-control"]').should('exist');
cy.get('[data-testid="panel-info"]').should('not.exist');
});
it('self contained info', () => {
cy.visit('http://localhost:4001/info');
cy.get('.App').should('not.contain', 'Event List');
cy.get('.App').should('not.contain', 'Timer Control');
cy.get('.App').should('not.contain', 'Messages Control');
cy.get('.App').should('contain', 'Info');
cy.get('[data-testid="panel-event-list"]').should('not.exist');
cy.get('[data-testid="panel-timer-control"]').should('not.exist');
cy.get('[data-testid="panel-messages-control"]').should('not.exist');
cy.get('[data-testid="panel-info"]').should('exist');
});
});
it('table routes', () => {
cy.visit('http://localhost:4001/table');
cy.contains('Running Timer');
cy.get('[data-testid="cuesheet"]').should('exist');
cy.visit('http://localhost:4001/cuesheet');
cy.contains('Running Timer');
cy.get('[data-testid="cuesheet"]').should('exist');
cy.visit('http://localhost:4001/cuelist');
cy.contains('Running Timer');
cy.get('[data-testid="cuesheet"]').should('exist');
});
});