v2 styling (#263)

* feat: implement progress bar on running event

* refactor: use event action hook

* refactor: align backend data

* style: v2 style tweaks

* style: v2 style tweaks + ts <Info>

* fix: fix delay increment calls

* style: v2 style tweaks + ts <Playback>

* style: v2 style tweaks + ts <EventEditor>

* style: v2 style tweaks + ts <MessageControl>

* fix: naming issue with message control socket endpoint

* chore: upgrade style dependencies

* style: v2 style tweaks + ts <MenuBar>

* style: v2 style tweaks + ts <RundownMenu>

* style: v2 style tweaks + ts <Playback>

* fix: add selected and next information to EventTimer

* style: v2 style tweaks + ts <EventBlock>

* style: v2 style tweaks + ts <DelayBlock>

* style: v2 style tweaks + ts <BlockBlock>

* style: v2 style tweaks + ts <QuickEntry>

* refactor: extract <TextInput> logic

* chore: upgrade build dependencies

* chore: folder structure refactor

* fix: issue with dependency path in electron

* chore: update version in demo db
This commit is contained in:
Carlos Valente
2022-11-27 14:56:09 +01:00
committed by GitHub
parent 3626b5b357
commit d486d78594
104 changed files with 3027 additions and 3217 deletions
+8 -8
View File
@@ -3,12 +3,12 @@
"version": "1.9.6",
"private": true,
"dependencies": {
"@chakra-ui/react": "2.3.4",
"@chakra-ui/react": "^2.4.1",
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@react-icons/all-files": "^4.1.0",
"@tanstack/react-query": "^4.10.3",
"@tanstack/react-query-devtools": "^4.11.0",
@@ -61,7 +61,7 @@
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"@vitejs/plugin-react": "^2.1.0",
"@vitejs/plugin-react": "^2.2.0",
"eslint": "^8.25.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.0.4",
@@ -77,10 +77,10 @@
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard-scss": "^4.0.0",
"typescript": "^4.8.3",
"vite": "^3.1.6",
"vite-plugin-svgr": "^2.2.1",
"vite-tsconfig-paths": "^3.5.0",
"vitest": "^0.23.2"
"vite": "^3.2.4",
"vite-plugin-svgr": "^2.2.2",
"vite-tsconfig-paths": "^3.6.0",
"vitest": "^0.25.3"
},
"resolutions": {
"**/@types/react": "18.0.21"
@@ -1,51 +0,0 @@
import { 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';
interface ActionButtonProps {
showAdd?: boolean;
showDelay?: boolean;
showBlock?: boolean;
actionHandler: (action: string) => void;
}
export default function ActionButtons(props: ActionButtonProps) {
const { showAdd, showDelay, showBlock, actionHandler } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...'>
<MenuButton
as={IconButton}
aria-label='Options'
size='sm'
icon={<FiPlus />}
colorScheme='white'
variant='outline'
/>
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
Add Event after
</MenuItem>
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')} isDisabled={!showDelay}>
Add Delay after
</MenuItem>
<MenuItem
icon={<FiMinusCircle />}
onClick={() => actionHandler('block')}
isDisabled={!showBlock}
>
Add Block after
</MenuItem>
</MenuList>
</Menu>
);
}
@@ -19,6 +19,23 @@ interface QuitIconBtnProps {
clickHandler: () => void;
size?: Size;
}
const quitBtnStyle = {
color: '#D20300', // $red-700
borderColor: '#D20300', // $red-700
_focus: { boxShadow: 'none' },
_hover: {
background: '#D20300', // $red-700
color: 'white',
},
_active: {
background: '#9A0000', // $red-1000
color: 'white',
},
variant: 'outline',
isRound: true,
};
export default function QuitIconBtn(props: QuitIconBtnProps) {
const { clickHandler, size = 'lg', ...rest } = props;
const [isOpen, setIsOpen] = useState(false);
@@ -47,11 +64,8 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
aria-label='Quit Application'
size={size}
icon={<FiPower />}
colorScheme='red'
variant='outline'
isRound
onClick={() => setIsOpen(true)}
_focus={{ boxShadow: 'none' }}
{...quitBtnStyle}
{...rest}
/>
</Tooltip>
@@ -59,7 +73,7 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Server Shutdown
Ontime Shutdown
</AlertDialogHeader>
<AlertDialogBody>
This will shutdown the program and all running servers. Are you sure?
@@ -1,25 +0,0 @@
import { Icon } from '@chakra-ui/react';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import PropTypes from 'prop-types';
import style from './CollapseBar.module.scss';
export default function CollapseBar(props) {
const { title = 'Collapse bar', isCollapsed, onClick } = props;
return (
<div className={style.header}>
{title}
<Icon
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
as={FiChevronUp}
onClick={onClick}
/>
</div>
);
}
CollapseBar.propTypes = {
title: PropTypes.string,
isCollapsed: PropTypes.bool,
onClick: PropTypes.func,
};
@@ -1,27 +1,23 @@
@use '../../../theme/main' as *;
@use '../../../theme/v2Styles' as *;
.header {
padding: 0;
margin: 0;
font-size: 0.9em;
font-size: $inner-section-text-size;
font-weight: 600;
display: flex;
justify-content: space-between;
color: $header-gray;
}
.moreExpanded,
.moreCollapsed {
color: $section-white;
border-bottom: 1px solid $border-color-ondark;
padding-bottom: $element-inner-spacing;
margin-bottom: $element-spacing;
cursor: pointer;
color: $text-white;
}
.moreExpanded {
transform: scaleY(-1);
transition: transform 0.3s;
transition: transform $transition-time-feedback;
}
.moreCollapsed {
transform: scaleY(1);
transition: transform 0.3s;
transition: transform $transition-time-feedback;
}
@@ -0,0 +1,20 @@
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import style from './CollapseBar.module.scss';
interface CollapseBarProps {
title: string;
isCollapsed: boolean;
onClick: () => void;
}
export default function CollapseBar(props: CollapseBarProps) {
const { title = 'Collapse bar', isCollapsed, onClick } = props;
return (
<div className={style.header} onClick={onClick}>
{title}
<FiChevronUp className={isCollapsed ? style.moreCollapsed : style.moreExpanded} />
</div>
);
}
@@ -0,0 +1,35 @@
import { PropsWithChildren } from 'react';
import { Button, ButtonGroup, IconButton, Tooltip } from '@chakra-ui/react';
import { IoCopy } from '@react-icons/all-files/io5/IoCopy';
import { tooltipDelayFast } from '../../../ontimeConfig';
import { Size } from '../../models/UtilTypes';
interface CopyTagProps {
label: string;
className?: string;
size?: Size;
}
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
const { label, className, size = 'xs', children } = props;
return (
<Tooltip label={label} openDelay={tooltipDelayFast}>
<ButtonGroup
size={size}
isAttached
className={className}
>
<Button variant='ontime-subtle' tabIndex={-1}>{children}</Button>
<IconButton
aria-label={label}
icon={<IoCopy />}
variant='ontime-filled'
tabIndex={-1}
onClick={() => navigator.clipboard.writeText(children as string)}
/>
</ButtonGroup>
</Tooltip>
);
}
@@ -1,7 +1,7 @@
@use '../../../theme/viewerDefs' as *;
.timer {
font-family: 'Open Sans', sans-serif;
font-family: var(--font-family-override, $ontime-font-family);
color: var(--timer-color-override, $viewer-color);
font-size: 21vw;
line-height: 21vw;
@@ -9,7 +9,7 @@
letter-spacing: 1vw;
&--small {
font-size: 4em;
font-size: 3.75em;
line-height: 0.9;
text-align: center;
letter-spacing: 0.1em;
@@ -1,27 +0,0 @@
import { useEffect, useRef } from 'react';
import { Textarea } from '@chakra-ui/react';
import autosize from 'autosize/dist/autosize';
export const AutoTextArea = (props) => {
const ref = useRef();
useEffect(() => {
const node = ref.current;
autosize(ref.current);
return () => {
autosize.destroy(node);
};
}, []);
return (
<Textarea
overflow='hidden'
w='100%'
resize='none'
ref={ref}
transition='height none'
{...props}
/>
);
};
@@ -1,106 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Input, Textarea } from '@chakra-ui/react';
import PropTypes from 'prop-types';
export default function TextInput(props) {
const { isTextArea, size = 'sm', field, initialText = '', submitHandler } = props;
const inputRef = useRef(null);
const [text, setText] = useState(initialText);
useEffect(() => {
if (typeof initialText === 'undefined') {
setText('');
} else {
setText(initialText);
}
}, [initialText]);
/**
* @description Handles Input value change
* @param {string} newValue
*/
const handleChange = useCallback(
(newValue) => {
if (newValue !== text) {
setText(newValue);
}
},
[text]
);
/**
* @description Handles submit events
* @param {string} valueToSubmit
*/
const handleSubmit = useCallback(
(valueToSubmit) => {
// No need to update if it hasn't changed
if (valueToSubmit === initialText) {
return;
}
const cleanVal = valueToSubmit.trim();
submitHandler(field, cleanVal);
if (cleanVal !== valueToSubmit) {
setText(cleanVal);
}
},
[field, initialText, submitHandler]
);
/**
* @description Resets input value to given
*/
const resetValue = useCallback(async () => {
setText(initialText);
},[initialText])
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const keyHandler = useCallback(
(event) => {
if (event.key === 'Escape') {
resetValue();
} else if (event.key === 'Enter') {
if (!isTextArea) {
handleSubmit(text);
}
}
},
[resetValue, isTextArea, handleSubmit, text]
);
return isTextArea ? (
<Textarea
ref={inputRef}
size={size}
variant='filled'
value={text}
onChange={(event) => handleChange(event.target.value)}
onBlur={(event) => handleSubmit(event.target.value)}
onKeyDown={(event) => keyHandler(event)}
data-testid='input-textarea'
/>
) : (
<Input
ref={inputRef}
size={size}
variant='filled'
value={text}
onChange={(event) => handleChange(event.target.value)}
onBlur={(event) => handleSubmit(event.target.value)}
onKeyDown={(event) => keyHandler(event)}
data-testid='input-textfield'
/>
);
}
TextInput.propTypes = {
isTextArea: PropTypes.bool,
size: PropTypes.string,
field: PropTypes.string.isRequired,
initialText: PropTypes.string,
submitHandler: PropTypes.func,
};
@@ -1,197 +0,0 @@
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
import { LoggingContext } from 'common/context/LoggingContext';
import { forgivingStringToMillis } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time';
import PropTypes from 'prop-types';
import { tooltipDelayFast } from '../../../ontimeConfig';
import style from './TimeInput.module.scss';
export default function TimeInput(props) {
const { name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd } = props;
const { emitError } = useContext(LoggingContext);
const inputRef = useRef(null);
const [value, setValue] = useState('');
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
// Todo: check if change is necessary
try {
setValue(stringFromMillis(time + delay));
} catch (error) {
emitError(`Unable to parse date: ${error.text}`);
}
}, [delay, emitError, time]);
/**
* @description Selects input text on focus
*/
const handleFocus = useCallback(() => {
inputRef.current.select();
}, []);
/**
* @description Submit handler
* @param {string} newValue
*/
const handleSubmit = useCallback(
(newValue) => {
// Check if there is anything there
if (newValue === '') {
return false;
}
let newValMillis = 0;
// check for known aliases
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
// string to pass should be the time of the end before
if (previousEnd != null) {
newValMillis = previousEnd;
}
} else if (
newValue.startsWith('+') ||
newValue.startsWith('p+') ||
newValue.startsWith('p +')
) {
// string to pass should add to the end before
const val = newValue.substring(1);
newValMillis = previousEnd + forgivingStringToMillis(val);
} else {
// convert entered value to milliseconds
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === originalMillis) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// update entry
submitHandler(name, newValMillis);
return true;
},
[delay, name, previousEnd, submitHandler, time, validationHandler]
);
/**
* @description Prepare time fields
* @param {string} value string to be parsed
*/
const validateAndSubmit = useCallback(
(newValue) => {
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(stringFromMillis(ms + delay));
} else {
resetValue();
}
},
[delay, handleSubmit, resetValue]
);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback(
(event) => {
if (event.key === 'Enter') {
inputRef.current.blur();
validateAndSubmit(event.target.value);
} else if (event.key === 'Tab') {
validateAndSubmit(event.target.value);
}
if (event.key === 'Escape') {
inputRef.current.blur();
resetValue();
}
},
[resetValue, validateAndSubmit]
);
useEffect(() => {
if (time == null) return;
resetValue();
}, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
/*
<IconButton
size='sm'
icon="s"
aria-label='automate'
colorScheme='white'
style={{ borderRadius: '2px', width: 'min-content' }}
tabIndex={-1}
variant='ghost'
/>
*/
const buttonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
if (name === 'duration') return 'D';
}
const buttonTooltip = () => {
if (name === 'timeStart') return 'Start';
if (name === 'timeEnd') return 'End';
if (name === 'duration') return 'Duration';
}
return (
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
<InputLeftElement width='fit-content'>
<Tooltip label={buttonTooltip()} openDelay={tooltipDelayFast}>
<Button
size='sm'
variant='filled'
tabIndex={-1}
backgroundColor='#303030'
color='#fffffa'
borderRadius='2px 0 0 2px'
border={isDelayed ? "1px solid #d69e2e55" : "1px solid transparent"}
>
{buttonInitial()}
</Button>
</Tooltip>
</InputLeftElement>
<Input
ref={inputRef}
data-testid='time-input'
className={style.inputField}
type='text'
placeholder={placeholder}
variant='filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={resetValue}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={8}
/>
</InputGroup>
);
}
TimeInput.propTypes = {
name: PropTypes.string,
submitHandler: PropTypes.func,
time: PropTypes.number,
delay: PropTypes.number,
placeholder: PropTypes.string,
validationHandler: PropTypes.func,
previousEnd: PropTypes.number,
};
@@ -1,47 +0,0 @@
@use '../../../theme/main' as *;
@mixin input-field {
border: 1px solid transparent;
background-color: $input-bg;
font-size: 1em;
letter-spacing: 1px;
&:hover {
background-color: $input-hover-bg;
}
}
.timeInput {
width: fit-content !important;
.inputField {
@include input-field;
width: 7.5em;
padding: 0 0 0 2.6em;
}
&.delayed {
.inputField {
border: $input-delayed-border;
background-color: $input-bg-delayed;
&:hover {
background-color: $input-hover-bg-delayed;
}
}
}
}
.delayInput {
display: flex;
align-items: center;
.inputField {
@include input-field;
}
.label {
padding-left: 8px;
font-size: 14px;
}
}
@@ -0,0 +1,34 @@
import { useEffect, useRef } from 'react';
import { Textarea, TextareaProps } from '@chakra-ui/react';
// @ts-expect-error no types from library
import autosize from 'autosize/dist/autosize';
interface AutoTextAreaProps extends TextareaProps {
isDark?: boolean;
}
export const AutoTextArea = (props: AutoTextAreaProps) => {
const { isDark, ...rest } = props;
const ref = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const node = ref.current;
autosize(ref.current);
return () => {
autosize.destroy(node);
};
}, []);
return (
<Textarea
overflow='hidden'
w='100%'
resize='none'
ref={ref}
transition='height none'
variant={isDark ? 'ontime-filled' : 'ontime-filled-onlight'}
{...rest}
/>
);
};
@@ -1,5 +1,3 @@
@use '../../../theme/main' as *;
input[type="color"] {
appearance: none;
cursor: pointer;
@@ -12,7 +12,7 @@ export default function ColourInput(props: ColourInputProps) {
return (
<Input
size='sm'
variant='filled'
variant='ontime-filled'
className={style.colourInput}
type='color'
value={value}
@@ -0,0 +1,9 @@
.delayInput {
display: flex;
gap: 8px;
align-items: center;
}
.inputField {
text-align: center;
}
@@ -3,24 +3,25 @@ import { Input } from '@chakra-ui/react';
import { clamp } from 'common/utils/math';
import PropTypes from 'prop-types';
import style from './TimeInput.module.scss';
import style from './DelayInput.module.scss';
const inputProps = {
const inputStyleProps = {
width: 20,
backgroundColor: 'rgba(255,255,255,0.13)',
color: '#fff',
border: '1px solid #ecc94b55',
variant: 'filled',
borderRadius: '3px',
placeholder: '-',
textAlign: 'center',
size: 'sm',
color: '#E69056',
variant: 'ontime-filled',
};
export default function DelayInput(props) {
const { submitHandler, value } = props;
interface DelayInputProps {
submitHandler: (value: number) => void;
value?: number;
}
export default function DelayInput(props: DelayInputProps) {
const { submitHandler, value = 0 } = props;
const [_value, setValue] = useState(value);
const inputRef = useRef(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (value == null) return;
@@ -32,7 +33,8 @@ export default function DelayInput(props) {
* @param {string} value string to be parsed
*/
const validate = useCallback(
(newValue) => {
(newValue?: string) => {
console.log('debug', newValue, typeof newValue);
if (newValue === '') setValue(0);
const delayValue = clamp(Number(newValue), -60, 60);
@@ -41,42 +43,42 @@ export default function DelayInput(props) {
submitHandler(delayValue);
},
[submitHandler, value]
[submitHandler, value],
);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback((event) => {
if (event.key === 'Enter') {
inputRef.current.blur();
validate(event.target.value);
} else if (event.key === 'Escape') {
inputRef.current.blur();
const onKeyDownHandler = useCallback((key: string) => {
if (key === 'Enter') {
inputRef.current?.blur();
validate(inputRef.current?.value);
} else if (key === 'Escape') {
inputRef.current?.blur();
setValue(value);
}
}, [validate, value]);
const labelText = `${Math.abs(value) > 1 ? 'minutes' : 'minute'} ${
value >= 0 ? 'delayed' : 'ahead'
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
}`;
return (
<div className={style.delayInput}>
<label className={style.delayInput}>
<Input
{...inputStyleProps}
ref={inputRef}
data-testid='delay-input'
className={style.inputField}
{...inputProps}
value={_value}
onChange={(event) => setValue(event.target.value)}
onBlur={() => setValue(value)}
onKeyDown={onKeyDownHandler}
onChange={(event) => setValue(Number(event.target.value))}
onBlur={(event) => validate(event.target.value)}
onKeyDown={(event) => onKeyDownHandler(event.key)}
type='number'
/>
<span className={style.label}>{labelText}</span>
</div>
{labelText}
</label>
);
}
@@ -0,0 +1,46 @@
import { useCallback, useRef } from 'react';
import { Input, Textarea } from '@chakra-ui/react';
import { Size } from '../../../models/UtilTypes';
import useReactiveTextInput from './useReactiveTextInput';
interface TextInputProps {
isTextArea?: boolean;
isFullHeight?: boolean;
size?: Size;
field: string;
initialText?: string;
submitHandler: (field: string, newValue: string) => void;
}
export default function TextInput(props: TextInputProps) {
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler } = props;
const inputRef = useRef(null);
const submitCallback = useCallback((newValue: string) =>
submitHandler(field, newValue)
, [field]);
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
return isTextArea ? (
<Textarea
ref={inputRef}
size={size}
variant='ontime-filled'
{...textAreaProps}
style={{ height: isFullHeight ? '100%' : undefined }}
data-testid='input-textarea'
/>
) : (
<Input
ref={inputRef}
size={size}
variant='ontime-filled'
{...textInputProps}
data-testid='input-textfield'
/>
);
}
@@ -9,7 +9,8 @@ describe('TextInput component', () => {
it('renders correctly', () => {
const testField = 'test';
const testText = 'Test 123';
render(<TextInput field={testField} initialText={testText} />);
const submitHandler = vi.fn();
render(<TextInput field={testField} initialText={testText} submitHandler={submitHandler} />);
const input = screen.getByTestId('input-textfield');
expect(input).toBeInTheDocument();
@@ -19,7 +20,9 @@ describe('TextInput component', () => {
it('Handles renders as textarea', () => {
const testField = 'test';
const testText = 'Test 123';
render(<TextInput field={testField} initialText={testText} isTextArea />);
const submitHandler = vi.fn();
render(<TextInput field={testField} initialText={testText} isTextArea
submitHandler={submitHandler} />);
const input = screen.getByTestId('input-textarea');
expect(input).toBeInTheDocument();
@@ -87,7 +90,7 @@ describe('TextInput component', () => {
it('handles undefined value', () => {
const testField = 'test';
const expected = '';
render(<TextInput field={testField} />);
render(<TextInput field={testField} submitHandler={vi.fn()} />);
const input = screen.getByTestId('input-textfield');
expect(input).toBeInTheDocument();
expect(input).toHaveValue(expected);
@@ -0,0 +1,88 @@
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
interface UseReactiveTextInputReturn {
value: string;
onChange: (event: ChangeEvent) => void;
onBlur: (event: ChangeEvent) => void;
onKeyDown: (event: KeyboardEvent) => void;
}
export default function useReactiveTextInput(
initialText: string,
submitCallback: (newValue: string) => void,
options?: {
submitOnEnter?: boolean;
},
): UseReactiveTextInputReturn {
const [text, setText] = useState(initialText);
useEffect(() => {
if (typeof initialText === 'undefined') {
setText('');
} else {
setText(initialText);
}
}, [initialText]);
/**
* @description Handles Input value change
* @param {string} newValue
*/
const handleChange = useCallback(
(newValue: string) => {
if (newValue !== text) {
setText(newValue);
}
},
[text],
);
/**
* @description Handles submit events
* @param {string} valueToSubmit
*/
const handleSubmit = useCallback(
(valueToSubmit: string) => {
// No need to update if it hasn't changed
if (valueToSubmit === initialText) {
return;
}
const cleanVal = valueToSubmit.trim();
submitCallback(cleanVal);
if (cleanVal !== valueToSubmit) {
setText(cleanVal);
}
},
[initialText, submitCallback],
);
/**
* @description Handles common keys for submit and cancel
* @param {string} key
*/
const keyHandler = useCallback(
(key: string) => {
switch (key) {
case 'Escape':
setText(initialText);
break;
case 'Enter':
if (options?.submitOnEnter) {
handleSubmit(text);
}
break;
}
},
[initialText, handleSubmit, text],
);
return {
value: text,
onChange: (event) => handleChange((event.target as HTMLInputElement).value),
onBlur: (event) => handleSubmit((event.target as HTMLInputElement).value),
onKeyDown: (event) => keyHandler(event.key),
};
}
@@ -0,0 +1,174 @@
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
import { LoggingContext } from 'common/context/LoggingContext';
import { forgivingStringToMillis } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time';
import PropTypes from 'prop-types';
import { tooltipDelayFast } from '../../../../ontimeConfig';
import style from './TimeInput.module.scss';
export default function TimeInput(props) {
const {
name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd,
} = props;
const { emitError } = useContext(LoggingContext);
const inputRef = useRef(null);
const [value, setValue] = useState('');
/**
* @description Resets input value to given
*/
const resetValue = useCallback(() => {
// Todo: check if change is necessary
try {
setValue(stringFromMillis(time + delay));
} catch (error) {
emitError(`Unable to parse date: ${error.text}`);
}
}, [delay, emitError, time]);
/**
* @description Selects input text on focus
*/
const handleFocus = useCallback(() => {
inputRef.current.select();
}, []);
/**
* @description Submit handler
* @param {string} newValue
*/
const handleSubmit = useCallback((newValue) => {
// Check if there is anything there
if (newValue === '') {
return false;
}
let newValMillis = 0;
// check for known aliases
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
// string to pass should be the time of the end before
if (previousEnd != null) {
newValMillis = previousEnd;
}
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
// string to pass should add to the end before
const val = newValue.substring(1);
newValMillis = previousEnd + forgivingStringToMillis(val);
} else {
// convert entered value to milliseconds
newValMillis = forgivingStringToMillis(newValue);
}
// Time now and time submittedVal
const originalMillis = time + delay;
// check if time is different from before
if (newValMillis === originalMillis) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// update entry
submitHandler(name, newValMillis);
return true;
}, [delay, name, previousEnd, submitHandler, time, validationHandler]);
/**
* @description Prepare time fields
* @param {string} value string to be parsed
*/
const validateAndSubmit = useCallback((newValue) => {
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(stringFromMillis(ms + delay));
} else {
resetValue();
}
}, [delay, handleSubmit, resetValue]);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback((event) => {
if (event.key === 'Enter') {
inputRef.current.blur();
validateAndSubmit(event.target.value);
} else if (event.key === 'Tab') {
validateAndSubmit(event.target.value);
}
if (event.key === 'Escape') {
inputRef.current.blur();
resetValue();
}
}, [resetValue, validateAndSubmit]);
useEffect(() => {
if (time == null) return;
resetValue();
}, [emitError, resetValue, time]);
const isDelayed = delay != null && delay !== 0;
const ButtonInitial = () => {
if (name === 'timeStart') return 'S';
if (name === 'timeEnd') return 'E';
if (name === 'duration') return 'D';
};
const ButtonTooltip = () => {
if (name === 'timeStart') return 'Start';
if (name === 'timeEnd') return 'End';
if (name === 'duration') return 'Duration';
};
return (
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
<InputLeftElement width='fit-content'>
<Tooltip label={<ButtonTooltip />} openDelay={tooltipDelayFast} variant='ontime-ondark'>
<Button
size='sm'
variant='ontime-subtle-white'
className={`${style.inputButton} ${isDelayed ? style.delayed : ''}`}
tabIndex={-1}
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
borderRight='1px solid transparent'
borderRadius='2px 0 0 2px'
>
<ButtonInitial />
</Button>
</Tooltip>
</InputLeftElement>
<Input
ref={inputRef}
data-testid='time-input'
className={style.inputField}
type='text'
placeholder={placeholder}
variant='ontime-filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={resetValue}
onKeyDown={onKeyDownHandler}
value={value}
maxLength={8}
/>
</InputGroup>
);
}
TimeInput.propTypes = {
name: PropTypes.string,
submitHandler: PropTypes.func,
time: PropTypes.number,
delay: PropTypes.number,
placeholder: PropTypes.string,
validationHandler: PropTypes.func,
previousEnd: PropTypes.number,
};
@@ -0,0 +1,19 @@
$input-font-size: 15px;
$input-delayed-border-color: #E69056;
.timeInput {
width: fit-content !important;
.inputField {
font-size: $input-font-size;
letter-spacing: 1px;
width: 7.5em;
padding: 0 0 0 2.6em;
}
&.delayed {
.inputField {
border: 1px solid $input-delayed-border-color;
}
}
}
@@ -1,23 +1,15 @@
$action-text-color: #87A3EF;
$border-color: rgba(white, 0.1);
$menu-bg: #202020;
$menu-hover-bg: #101010;
$menu-focus-bg: #181818;
$menu-box-shadow: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
@use "../../../theme/v2Styles" as *;
@use "../../../theme/mixins" as *;
@use "../../../theme/ontimeColours" as *;
$icon-color: #f6f6f6;
$button-bg: #303030;
$menu-bg: $gray-1200;
$menu-hover-bg: $gray-1350;
$menu-focus-bg: $gray-1300;
$icon-color: $ui-white;
$button-bg: $gray-1050;
$button-size: 48px;
$ontime-pink: #ff7597;
// Todo: remove important once style is retired
a {
&::after {
content: none !important;
}
}
.mirror {
transform: rotate(180deg);
}
@@ -47,15 +39,14 @@ a {
.menuContainer {
top: 0;
left: 0;
font-family: 'Open Sans', sans-serif;
height: fit-content;
position: absolute;
background-color: $menu-bg;
min-width: 200px;
border-radius: 0 0 24px 0;
border-right: 1px solid $border-color;
border-right: 1px solid $border-color-ondark;
box-shadow: $menu-box-shadow;
box-shadow: $box-shadow-l2;
padding-bottom: 1rem;
max-height: 100vh;
@@ -67,21 +58,17 @@ a {
}
.link {
color: $action-text-color;
display: flex;
align-items: center;
@include action-link;
justify-content: space-between;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 14px;
&:hover {
background-color: $menu-hover-bg;
color: $ontime-pink;
}
&:active {
background-color: $border-color;
background-color: $border-color-ondark;
}
&:focus {
@@ -96,12 +83,11 @@ a {
}
}
.linkIcon {
display: inline-block;
transform: rotate(45deg);
}
.separator {
border-color: $border-color;
border-color: $border-color-ondark;
}
@@ -1,26 +0,0 @@
@use '../../../theme/main' as *;
.copyTag {
display: flex;
border-radius: 2px;
border: 1px solid $action-blue;
cursor: pointer;
font-size: 0.75em;
width: max-content;
&:active {
border: 1px solid $text-white;
}
}
.label {
background-color: $action-blue;
color: $text-white;
padding: 0 4px;
font-weight: 600;
}
.text {
color: $label-gray;
padding: 0 4px;
}
@@ -1,34 +0,0 @@
import { PropsWithChildren } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { tooltipDelayFast } from '../../../ontimeConfig';
import style from './CopyTag.module.scss';
interface CopyTagProps {
label?: string;
className?: string;
}
export default function CopyTag(props: PropsWithChildren<CopyTagProps>) {
const { label, className, children } = props;
return (
<Tooltip label='Click to copy' openDelay={tooltipDelayFast}>
<button
className={`${style.copyTag} ${className}`}
onClick={() => navigator.clipboard.writeText(children as string)}
tabIndex={-1}
>
{label && (
<span className={style.label}>
{label}
</span>
)}
<span className={style.text}>
{children}
</span>
</button>
</Tooltip>
);
}
+11 -6
View File
@@ -1,11 +1,16 @@
import { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
import { generateId } from '../utils/generate_id';
import socket from '../utils/socket';
import { nowInMillis, stringFromMillis } from '../utils/time';
import socket from '../utils/socket';
type LOG_LEVEL = 'INFO' | 'WARN' | 'ERROR';
type Log = {
export enum LOG_LEVEL {
INFO = "INFO",
WARN = "WARN",
ERROR = "ERROR",
}
export type Log = {
id: string;
origin: string;
time: string;
@@ -90,7 +95,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
*/
const emitInfo = useCallback(
(text: string) => {
_send(text, 'INFO');
_send(text, LOG_LEVEL.INFO);
},
[_send],
);
@@ -101,7 +106,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
*/
const emitWarning = useCallback(
(text: string) => {
_send(text, 'WARN');
_send(text, LOG_LEVEL.WARN);
},
[_send],
);
@@ -112,7 +117,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
*/
const emitError = useCallback(
(text: string) => {
_send(text, 'ERROR');
_send(text, LOG_LEVEL.ERROR);
},
[_send],
);
+2 -2
View File
@@ -46,7 +46,7 @@ const emptyMessageControl = {
};
export const useMessageControl = createSocketHook(FEAT_MESSAGECONTROL, emptyMessageControl);
export const setMessage = () => ({
export const setMessage = {
presenterText: (payload: string) => socket.emit('set-timer-message-text', payload),
presenterVisible: (payload: boolean) => socket.emit('set-timer-message-visible', payload),
publicText: (payload: string) => socket.emit('set-public-message-text', payload),
@@ -54,7 +54,7 @@ export const setMessage = () => ({
lowerText: (payload: string) => socket.emit('set-lower-message-text', payload),
lowerVisible: (payload: boolean) => socket.emit('set-lower-message-visible', payload),
onAir: (payload: boolean) => socket.emit('set-onAir', payload),
});
};
export const emptyPlaybackControl = {
playback: 'stop',
+7 -2
View File
@@ -1,8 +1,13 @@
import { OntimeSettingsType } from './OntimeSettings.type';
type NetworkInterfaceType = {
name: string;
address: string;
}
export type InfoType = {
networkInterfaces: string[];
settings: Pick<OntimeSettingsType, "version" | "serverPort" >
networkInterfaces: NetworkInterfaceType[];
settings: Pick<OntimeSettingsType, 'version' | 'serverPort'>
}
export const ontimePlaceholderInfo: InfoType = {
@@ -1,39 +0,0 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
import MessageControl from '../message/MessageControl';
// need to inject the socket provider to make component
// render without failing
const MockMessageControl = () => {
return (
<QueryClientProvider client={queryClientMock}>
<MessageControl />
</QueryClientProvider>
);
};
describe('Message Control input blocks', () => {
test('Presenter dialog', async () => {
// Presenter dialog and button
// substring match, ignore case
render(<MockMessageControl />);
expect(screen.getByPlaceholderText(/stage/i)).toBeInTheDocument();
});
test('Public dialog', async () => {
// Public dialog and button
// substring match, ignore case
render(<MockMessageControl />);
expect(screen.getByPlaceholderText(/public/i)).toBeInTheDocument();
});
test('Lower third', async () => {
// Lower third dialog and button
// substring match, ignore case
render(<MockMessageControl />);
expect(screen.getByPlaceholderText(/lower third/i)).toBeInTheDocument();
});
});
@@ -1,21 +0,0 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
import PlaybackControl from '../playback/PlaybackControl';
test('check that playback control renders', async () => {
// need to inject the socket provider to make component
// render without failing
render(
<QueryClientProvider client={queryClientMock}>
<PlaybackControl />
</QueryClientProvider>,
);
// Text labels for times
// substring match, ignore case
expect(screen.getByText(/started/i)).toBeInTheDocument();
expect(screen.getByText(/finish/i)).toBeInTheDocument();
});
@@ -0,0 +1,17 @@
@use '../../../theme/v2Styles' as *;
.inputItems {
display: grid;
grid-template-columns: 1fr auto;
gap: $element-spacing;
margin-top: $element-inner-spacing;
}
.label {
font-size: $inner-section-text-size;
color: $label-gray;
&.active {
color: $action-text-color;
}
}
@@ -1,16 +1,17 @@
import { Input } from '@chakra-ui/react';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { IoEye } from '@react-icons/all-files/io5/IoEye';
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './MessageControl.module.scss';
import style from './InputRow.module.scss';
interface InputRowProps {
label: string;
placeholder: string;
text: string;
visible: boolean;
visible?: boolean;
actionHandler: (action: string, payload: object) => void;
changeHandler: (newValue: string) => void;
}
@@ -23,12 +24,12 @@ export default function InputRow(props: InputRowProps) {
};
return (
<div className={`${visible ? style.inputRowActive : ''}`}>
<label className={style.label}>{label}</label>
<div className={style.inputRow}>
<label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label>
<div className={style.inputItems}>
<Input
size='sm'
variant='filled'
variant='ontime-filled'
value={text}
onChange={(event) => handleInputChange(event.target.value)}
placeholder={placeholder}
@@ -38,9 +39,8 @@ export default function InputRow(props: InputRowProps) {
tooltip={visible ? 'Make invisible' : 'Make visible'}
aria-label={`Toggle ${label}`}
openDelay={tooltipDelayMid}
icon={<IoSunny size='18px' />}
colorScheme='blue'
variant={visible ? 'solid' : 'outline'}
icon={visible? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
size='sm'
/>
</div>
@@ -1,64 +0,0 @@
import { IconButton, Tooltip } from '@chakra-ui/react';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
import CopyTag from '../../../common/components/osc-tag/CopyTag';
import { setMessage, useMessageControl } from '../../../common/hooks/useSocket';
import { tooltipDelayMid } from '../../../ontimeConfig';
import InputRow from './InputRow';
import style from './MessageControl.module.scss';
export default function MessageControl() {
const { data } = useMessageControl();
return (
<>
<div className={style.messageContainer}>
<InputRow
label='Timer screen message'
placeholder='Shown in stage timer'
text={data.presenter.text}
visible={data.presenter.visible}
changeHandler={(newValue) => setMessage.presenterText(newValue)}
actionHandler={() => setMessage.presenterVisible(!data.presenter.visible)}
/>
<InputRow
label='Public screen message'
placeholder='Shown in public screens'
text={data.public.text}
visible={data.public.visible}
changeHandler={(newValue) => setMessage.publicText(newValue)}
actionHandler={() => setMessage.publicVisible(!data.public.visible)}
/>
<InputRow
label='Lower third message'
placeholder='Shown in lower third'
text={data.lower.text}
visible={data.lower.visible}
changeHandler={(newValue) => setMessage.lowerText(newValue)}
actionHandler={() => setMessage.lowerVisible(!data.lower.visible)}
/>
</div>
<div className={style.onAirToggle}>
<Tooltip label={data.onAir ? 'Go Off Air' : 'Go On Air'} openDelay={tooltipDelayMid}>
<IconButton
className={style.btn}
size='md'
icon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
colorScheme='blue'
variant={data.onAir ? 'solid' : 'outline'}
onClick={() => setMessage.onAir(!data.onAir)}
aria-label='Toggle On Air'
/>
</Tooltip>
<div className={style.onAirLabel}>On Air</div>
<div className={style.oscLabel}>
<CopyTag label='OSC'>/ontime/offAir</CopyTag>
<CopyTag label='OSC'>/ontime/offAir</CopyTag>
</div>
</div>
</>
);
}
@@ -1,66 +1,23 @@
@use '../../../theme/main' as *;
@use '../../../theme/mixins' as *;
@mixin message-control-label() {
font-size: 0.9em;
color: $label-gray;
}
.messageContainer,
.onAirToggle {
display: flex;
padding: 0.5em;
}
@use '../../../theme/v2Styles' as *;
.messageContainer {
display: flex;
flex-direction: column;
gap: 4px;
.inputItems {
display: grid;
grid-template-columns: 1fr auto;
gap: 1em;
}
.label {
padding: 0;
margin: 0;
@include message-control-label;
}
.inputRowActive {
.label {
color: $action-blue;
}
}
gap: $section-spacing;
}
.onAirToggle {
margin-top: 1em;
display: grid;
grid-template-areas:
'btn label'
'btn osc';
grid-template-columns: auto 1fr;
grid-template-rows: 1fr 1fr;
align-items: center;
.btn {
aspect-ratio: 1;
grid-area: btn;
height: 100%;
margin-right: 16px;
}
.onAirLabel {
grid-area: label;
@include message-control-label;
}
.oscLabel {
grid-area: osc;
display: flex;
gap: 4px;
}
.onAirSection {
margin-top: $section-spacing;
display: flex;
flex-direction: column;
gap: $element-spacing;
}
.label {
font-size: $inner-section-text-size;
color: $label-gray;
&.active {
color: $action-text-color;
}
}
@@ -0,0 +1,52 @@
import { Button } from '@chakra-ui/react';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
import { setMessage, useMessageControl } from '../../../common/hooks/useSocket';
import InputRow from './InputRow';
import style from './MessageControl.module.scss';
export default function MessageControl() {
const { data } = useMessageControl();
return (
<div className={style.messageContainer}>
<InputRow
label='Timer screen message'
placeholder='Shown in stage timer'
text={data?.presenter.text || ''}
visible={data?.presenter.visible || false}
changeHandler={(newValue) => setMessage.presenterText(newValue)}
actionHandler={() => setMessage.presenterVisible(!data?.presenter.visible)}
/>
<InputRow
label='Public screen message'
placeholder='Shown in public screens'
text={data?.public.text || ''}
visible={data?.public.visible || false}
changeHandler={(newValue) => setMessage.publicText(newValue)}
actionHandler={() => setMessage.publicVisible(!data?.public.visible)}
/>
<InputRow
label='Lower third message'
placeholder='Shown in lower third'
text={data?.lower.text || ''}
visible={data?.lower.visible || false}
changeHandler={(newValue) => setMessage.lowerText(newValue)}
actionHandler={() => setMessage.lowerVisible(!data?.lower.visible)}
/>
<div className={style.onAirSection}>
<label className={style.label}>Toggle On Air state</label>
<Button
variant={data?.onAir ? 'ontime-filled' : 'ontime-subtle'}
leftIcon={data?.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
onClick={() => setMessage.onAir(!data?.onAir)}
>
{data?.onAir ? 'Ontime is On Air' : 'Ontime is Off Air'}
</Button>
</div>
</div>
);
}
@@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
import { handleLinks } from '../../../common/utils/linkUtils';
@@ -11,7 +11,7 @@ import style from '../../editors/Editor.module.scss';
export default function MessageControlExport() {
return (
<Box className={style.messages} data-testid="panel-messages-control">
<FiArrowUpRight className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'messagecontrol')} />
<div className={style.content}>
<ErrorBoundary>
<MessageControl />
@@ -1,11 +1,9 @@
import { Tooltip } from '@chakra-ui/react';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { setPlayback } from '../../../common/hooks/useSocket';
import { Playstate } from '../../../common/models/OntimeTypes';
import { tooltipDelayMid } from '../../../ontimeConfig';
import TapButton from './TapButton';
@@ -13,48 +11,44 @@ import style from './PlaybackControl.module.scss';
interface PlaybackProps {
playback: Playstate;
selectedId: string;
selectedId: string | null;
noEvents: boolean;
}
export default function Playback(props: PlaybackProps) {
const { playback, selectedId, noEvents } = props;
const isRolling = playback === 'roll';
const isPlaying = playback === 'start';
const isPaused = playback === 'pause';
return (
<div className={style.playbackContainer}>
<Tooltip label='Start playback' openDelay={100}>
<TapButton
onClick={() => setPlayback.start()}
disabled={!selectedId || isRolling || noEvents}
theme='start'
active={playback === 'start'}
>
<IoPlay />
</TapButton>
</Tooltip>
<TapButton
onClick={() => setPlayback.start()}
disabled={!selectedId || isRolling || noEvents}
theme='start'
active={isPlaying}
>
<IoPlay />
</TapButton>
<Tooltip label='Pause playback' openDelay={tooltipDelayMid}>
<TapButton
onClick={() => setPlayback.pause()}
disabled={!selectedId || isRolling || noEvents || playback !== 'start'}
theme='pause'
active={playback === 'pause'}
>
<IoPause />
</TapButton>
</Tooltip>
<TapButton
onClick={() => setPlayback.pause()}
disabled={!selectedId || isRolling || noEvents}
theme='pause'
active={isPaused}
>
<IoPause />
</TapButton>
<Tooltip label='Start roll mode' openDelay={tooltipDelayMid}>
<TapButton
onClick={() => setPlayback.roll()}
disabled={playback === 'roll' || noEvents}
theme='roll'
active={isRolling}
>
<IoTimeOutline />
</TapButton>
</Tooltip>
<TapButton
onClick={() => setPlayback.roll()}
disabled={noEvents}
theme='roll'
active={isRolling}
>
<IoTimeOutline />
</TapButton>
</div>
);
}
@@ -1,42 +0,0 @@
import { memo } from 'react';
import PropTypes from 'prop-types';
import Playback from './Playback';
import Transport from './Transport';
const areEqual = (prevProps, nextProps) => {
return (
prevProps.playback === nextProps.playback &&
prevProps.selectedId === nextProps.selectedId &&
prevProps.noEvents === nextProps.noEvents
);
};
const PlaybackButtons = (props) => {
const { playback, selectedId, noEvents, playbackControl } = props;
return (
<>
<Playback
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
playbackControl={playbackControl}
/>
<Transport
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
playbackControl={playbackControl}
/>
</>
);
};
export default memo(PlaybackButtons, areEqual);
PlaybackButtons.propTypes = {
playback: PropTypes.string,
selectedId: PropTypes.string,
playbackControl: PropTypes.object,
noEvents: PropTypes.bool.isRequired,
};
@@ -0,0 +1,28 @@
import { Playstate } from '../../../common/models/OntimeTypes';
import Playback from './Playback';
import Transport from './Transport';
interface PlaybackButtonsProps {
playback: Playstate;
selectedId: string | null;
noEvents: boolean;
}
export default function PlaybackButtons(props: PlaybackButtonsProps) {
const { playback, selectedId, noEvents } = props;
return (
<>
<Playback
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
/>
<Transport
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
/>
</>
);
};
@@ -1,14 +1,14 @@
@use '../../../theme/main' as *;
@use '../../../theme/v2Styles' as *;
@use '../../../theme/ontimeColours' as *;
@use '../../../theme/mixins' as *;
.mainContainer {
width: 100%;
display: grid;
margin: 0 auto;
gap: 4px;
gap: $element-inner-spacing;
}
.timeContainer {
display: grid;
grid-template-areas:
@@ -16,7 +16,7 @@
'... sta fin btn';
grid-template-rows: 1fr auto;
grid-template-columns: 1.5em 1fr 1fr 5em;
gap: 4px;
gap: $element-inner-spacing;
justify-items: start;
}
@@ -38,16 +38,16 @@
.indRoll,
.indDelay,
.indNegative {
background-color: $bg-black-300;
background-color: $gray-1300;
}
.indRoll,
.indRollActive,
.indDelay {
margin: 0 auto;
border-radius: 50%;
width: 0.8em;
height: 0.8em;
border-radius: 6px;
width: 12px;
height: 12px;
}
.indRollActive {
@@ -58,11 +58,11 @@
.indNegativeActive {
margin: 0 auto;
width: 90%;
height: 0.3em;
height: 4px;
}
.indNegativeActive {
background-color: $ontime-pink-variant;
background-color: $playback-negative;
}
.indDelayActive {
@@ -75,7 +75,7 @@
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
width: 100%;
gap: 4px;
gap: $element-inner-spacing;
}
.minus {
@@ -103,23 +103,27 @@
}
.time {
color: $header-gray;
font-size: 1.1em;
color: $section-white;
font-size: $text-body-size;
}
.tag {
color: $label-gray;
font-size: 0.9em;
font-size: 13px;
}
.rolltag {
color: $ontime-roll;
font-size: 0.9em;
font-size: $text-body-size;
}
.playbackContainer {
display: flex;
justify-content: space-evenly;
padding-top: 0.5em;
gap: 10px;
gap: $element-spacing;
}
.invertX {
transform: rotateY(180deg);
}
@@ -59,7 +59,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
</>
)}
<div className={style.btn}>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid}
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}>
<TapButton
onClick={() => setPlayback.delay(-5)}
@@ -68,33 +68,33 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
-5
</TapButton>
</Tooltip>
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}>
<TapButton
onClick={() => setPlayback.delay(+5)}
disabled={disableButtons}
square>
5
</TapButton>
</Tooltip>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}>
<TapButton
onClick={() => setPlayback.delay(-5)}
onClick={() => setPlayback.delay(-1)}
disabled={disableButtons}
square>
-1
</TapButton>
</Tooltip>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid}
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}>
<TapButton
onClick={() => setPlayback.delay(-5)}
onClick={() => setPlayback.delay(1)}
disabled={disableButtons}
square>
1
</TapButton>
</Tooltip>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}>
<TapButton
onClick={() => setPlayback.delay(-5)}
disabled={disableButtons}
square>
5
</TapButton>
</Tooltip>
</div>
</div>
);
@@ -1,62 +1,81 @@
@use '../../../theme/main' as *;
@use '../../../theme/v2Styles' as *;
@use '../../../theme/ontimeColours' as *;
@mixin tap-factory($theme-front, $theme-bg, $theme-high) {
font-family: "Open Sans", sans-serif;
$button-bg-gray: $gray-1050;
$button-color-white: $gray-50;
@mixin tap-factory($theme-color) {
font-family: $ontime-font-family;
font-size: 22px;
background-color: $theme-bg;
color: $theme-front;
border-radius: 3px;
border-radius: $component-border-radius-md;
width: 100%;
aspect-ratio: 3/1;
box-shadow: $theme-high 0 0 2px;
transition: background-color 0.3s;
transition-property: color, background-color;
transition-duration: $transition-time-feedback;
display: grid;
place-content: center;
letter-spacing: 0.3px;
background-color: $button-bg-gray;
color: $theme-color;
&:disabled {
cursor: not-allowed;
background-color: $opacity-disabled;
box-shadow: none;
opacity: $opacity-disabled;
}
&:hover:not(:disabled) {
background-color: $theme-high;
color: $button-color-white;
background-color: $theme-color;
}
&:active:not(:disabled) {
color: $theme-bg;
background-color: $theme-front;
transition: background-color 0.15s;
color: $button-bg-gray;
background-color: $button-color-white;
transition-property: color, background-color;
transition-duration: $transition-time-action;
}
&.active {
background-color: $theme-high;
background-color: $theme-color;
color: $button-color-white;
}
}
.tapButton.neutral{
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, #363636);
.tapButton.neutral {
@include tap-factory($gray-50);
&:hover:not(:disabled) {
color: $gray-50;
background-color: $gray-1000;
}
&:active:not(:disabled) {
color: $button-bg-gray;
background-color: $button-color-white;
transition-property: color, background-color;
transition-duration: $transition-time-action;
}
}
.tapButton.start {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-accent);
@include tap-factory($playback-start);
}
.tapButton.roll {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-roll);
@include tap-factory($ontime-roll);
}
.tapButton.pause {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-paused);
@include tap-factory($ontime-paused);
}
.tapButton.ontime {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-pink);
@include tap-factory($ontime-color);
}
.tapButton.stop {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-red);
@include tap-factory($ontime-stop);
}
.tapButton.square {
@@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
import { handleLinks } from '../../../common/utils/linkUtils';
@@ -10,8 +10,8 @@ import style from '../../editors/Editor.module.scss';
export default function TimerControlExport() {
return (
<Box className={style.playback} data-testid="panel-timer-control">
<FiArrowUpRight className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
<Box className={style.playback} data-testid='panel-timer-control'>
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
<div className={style.content}>
<ErrorBoundary>
<PlaybackControl />
@@ -1,7 +1,7 @@
import { Tooltip } from '@chakra-ui/react';
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 { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { setPlayback } from '../../../common/hooks/useSocket';
@@ -14,7 +14,7 @@ import style from './PlaybackControl.module.scss';
interface TransportProps {
playback: Playstate;
selectedId: string;
selectedId: string | null;
noEvents: boolean;
}
@@ -45,10 +45,10 @@ export default function Transport(props: TransportProps) {
onClick={() => setPlayback.reload()}
disabled={selectedId == null || isRolling || noEvents}
>
<IoPlayBack />
<IoReload className={style.invertX} />
</TapButton>
</Tooltip>
<Tooltip label='Unload Event' openDelay={100}>
<Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton
onClick={() => setPlayback.stop()}
disabled={(selectedId == null && !isRolling) || noEvents}
+11 -9
View File
@@ -5,12 +5,13 @@
top: $distance;
right: $distance;
cursor: pointer;
color: $ontime-pink;
color: #f2f2f2;
}
.corner {
display: none;
@include absolute-top-right(8px);
transform: rotate(45deg);
}
.mainContainer {
@@ -20,10 +21,11 @@
margin: auto;
color: $title-white;
padding: max(16px, 2vh) max(8px, 1vh);
font-family: "Open Sans", "Segoe UI", sans-serif;
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: 48px 46em 30em auto;
grid-template-columns: 48px 46em 450px auto;
grid-template-areas:
'sett even play info'
'sett even mess info';
@@ -130,17 +132,17 @@
}
.eventEditor {
border-radius: 3px 3px 0 0;
background-color: $bg-black;
border-radius: 8px 8px 0 0;
background-color: #202020;
box-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px;
border-top: 1px solid $bg-gray-900;
position: absolute;
bottom: 0;
width: 100vw;
max-height: 40vh;
left: 0;
z-index: 10;
color: white;
transition: bottom 0.3s;
transition: bottom 0.3s; // $transition-time
&.noEvent {
bottom: -500px;
@@ -152,7 +154,7 @@
}
.header {
background-color: $bg-black;
background-color: #202020;
padding: 8px;
border-left: 1px solid $bg-container-l3;
}
@@ -186,9 +188,9 @@
.playback {
grid-area: play;
min-height: 275px;
min-height: 250px;
max-height: 380px;
min-width: 480px;
min-width: 450px;
}
.mainContainer > .settings {
@@ -1,11 +1,10 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { Button, Select } from '@chakra-ui/react';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { Button, Select, Switch } from '@chakra-ui/react';
import { IoBan } from '@react-icons/all-files/io5/IoBan';
import { editorEventId } from 'common/atoms/LocalEventSettings';
import ColourInput from 'common/components/input/ColourInput';
import TextInput from 'common/components/input/TextInput';
import TimeInput from 'common/components/input/TimeInput';
import ColourInput from 'common/components/input/colour-input/ColourInput';
import TextInput from 'common/components/input/text-input/TextInput';
import TimeInput from 'common/components/input/time-input/TimeInput';
import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig';
@@ -14,10 +13,10 @@ import { stringFromMillis } from 'common/utils/time';
import { calculateDuration, validateEntry } from 'common/utils/timesManager';
import { useAtom } from 'jotai';
import CopyTag from '../../common/components/copy-tag/CopyTag';
import useRundown from '../../common/hooks-query/useRundown';
import style from './EventEditor.module.scss';
import CopyTag from '../../common/components/osc-tag/CopyTag';
export default function EventEditor() {
const [openId] = useAtom(editorEventId);
@@ -93,7 +92,7 @@ export default function EventEditor() {
);
if (!event) {
return <span>Loading</span>;
return <span>Loading...</span>;
}
const delayed = delay !== 0;
@@ -105,6 +104,10 @@ export default function EventEditor() {
return (
<div className={style.eventEditor}>
<div className={style.eventInfo}>{`Event ${'not yet'} | Event ID ${event.id}`}</div>
<div className={style.eventActions}>
<CopyTag label='OSC trigger'>{`/ontime/gotoid/${event.id}`}</CopyTag>
</div>
<div className={style.timeOptions}>
<div className={style.timers}>
<label className={style.inputLabel}>
@@ -116,7 +119,7 @@ export default function EventEditor() {
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.timeStart}
delay={0}
delay={delay}
placeholder='Start'
/>
<label className={style.inputLabel}>
@@ -128,7 +131,7 @@ export default function EventEditor() {
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.timeEnd}
delay={0}
delay={delay}
placeholder='End'
/>
<label className={style.inputLabel}>Duration</label>
@@ -137,24 +140,33 @@ export default function EventEditor() {
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={event.duration}
delay={0}
delay={delay}
placeholder='Duration'
/>
</div>
<div className={style.timeSettings}>
<label className={style.inputLabel}>Timer type</label>
<Select size='sm' variant='filled' color='black'>
<Select size='sm' variant='ontime'>
<option value='option1'>Start to end</option>
<option value='option2'>Duration</option>
<option value='option3'>Follow previous</option>
<option value='option3'>Start only</option>
</Select>
<label className={style.inputLabel}>Countdown style</label>
<Select size='sm' variant='filled' color='black'>
<Select size='sm' variant='ontime'>
<option value='option1'>Count down</option>
<option value='option2'>Count up</option>
<option value='option3'>Clock</option>
</Select>
<span className={style.spacer} />
<label className={`${style.inputLabel} ${style.publicToggle}`}>
<Switch
isChecked={event.isPublic}
onChange={() => togglePublic(event.isPublic)}
variant='ontime'
/>
Event is public
</label>
</div>
</div>
<div className={style.titles}>
@@ -175,17 +187,6 @@ export default function EventEditor() {
<label className={style.inputLabel}>Subtitle</label>
<TextInput field='subtitle' initialText={event.subtitle} submitHandler={handleSubmit} />
</div>
<div className={style.padTop}>
<Button
leftIcon={<FiUsers />}
size='sm'
colorScheme='blue'
variant={event.isPublic ? 'solid' : 'ghost'}
onClick={() => togglePublic(event.isPublic)}
>
{event.isPublic ? 'Event is Public' : 'Make event public'}
</Button>
</div>
</div>
<div className={style.right}>
<div className={style.column}>
@@ -196,28 +197,26 @@ export default function EventEditor() {
handleChange={(value) => handleSubmit('colour', value)}
/>
<Button
leftIcon={<IoBan />}
rightIcon={<IoBan />}
onClick={() => handleSubmit('colour', '')}
variant='ghost'
colorScheme='blue'
borderRadius='3px'
variant='ontime-subtle'
size='sm'
>
Clear colour
</Button>
</div>
</div>
<div className={style.column}>
<div className={`${style.column} ${style.fullHeight}`}>
<label className={style.inputLabel}>Note</label>
<TextInput
field='note'
initialText={event.note}
submitHandler={handleSubmit}
isTextArea
isFullHeight
/>
</div>
</div>
<CopyTag label='OSC trigger' className={style.osc}>{`/ontime/gotoid/${event.id}`}</CopyTag>
</div>
</div>
);
@@ -1,102 +1,104 @@
@use '../../theme/main' as *;
@use '../../theme/v2Styles' as *;
.eventEditor {
padding: 16px 32px;
padding: 16px 32px 32px 32px;
width: 100%;
overflow-y: scroll;
max-height: 40vh;
gap: max(16px, 2vh);
display: grid;
grid-template-areas: 'timeOptions titles';
grid-template-areas:
'eventInfo eventActions'
'timeOptions titles';
grid-template-columns: auto 1fr;
.timeOptions,
.titles {
background-color: $bg-container-over;
border-radius: 2px;
padding: 8px 16px;
}
.timeOptions {
grid-area: timeOptions;
display: flex;
gap: 24px;
}
.timers, .timeSettings {
display: flex;
flex-direction: column;
gap: 8px;
}
.timers label:nth-child(1),
.timeSettings label:nth-child(1),
{
margin-top: 4px;
}
.timers,
.timeSettings {
display: flex;
flex-direction: column;
gap: 8px;
}
}
.titles {
grid-area: titles;
display: grid;
grid-template-areas:
'left right'
'tag tag';
grid-template-columns: 1fr 1fr;
.eventInfo {
grid-area: eventInfo;
}
.left,
.right {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.eventActions {
grid-area: eventActions;
margin-left: auto;
}
.left {
grid-area: left;
padding-right: 16px;
}
.timeOptions {
grid-area: timeOptions;
display: flex;
gap: 24px;
}
.right {
padding-left: 16px;
grid-area: right;
border-left: 1px solid $bg-container-l3;
}
.titles {
grid-area: titles;
display: grid;
grid-template-areas: 'left right';
grid-template-columns: 1fr 1fr;
.osc {
grid-area: tag;
justify-self: end;
}
}
.inline {
display: flex;
align-items: center;
gap: 16px;
}
.column {
.left,
.right {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.padTop {
margin-top: 8px;
.left {
grid-area: left;
padding: 0 16px;
border-left: 1px solid $border-color-ondark;
}
.inputLabel {
font-size: 13px;
display: block;
color: $label-gray;
.delayLabel {
color: $text-delay;
}
.right {
padding-left: 16px;
grid-area: right;
border-left: 1px solid $border-color-ondark;
}
}
.inputLabel {
font-size: 13px;
display: block;
color: $label-gray;
.delayLabel {
color: $ontime-delay-text;
}
&.publicToggle {
height: 32px;
display: flex;
align-items: center;
justify-items: center;
gap: 8px;
}
}
.spacer {
height: 20px;
}
.inline {
display: flex;
align-items: center;
gap: 16px;
}
.column {
display: flex;
flex-direction: column;
gap: 8px;
}
.padTop {
margin-top: 8px;
}
.fullHeight {
height: 100%
}
@@ -0,0 +1,52 @@
import { useState } from 'react';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import style from './Info.module.scss';
type TitleShape = {
title: string;
presenter: string;
subtitle: string;
note: string;
}
interface CollapsableInfoProps {
title: string;
data: TitleShape;
}
export default function CollapsableInfo(props: CollapsableInfoProps) {
const { title, data } = props;
const [collapsed, setCollapsed] = useState(false);
return (
<div className={style.container}>
<CollapseBar
title={title}
isCollapsed={collapsed}
onClick={() => setCollapsed((prev) => !prev)}
/>
{!collapsed && (
<div className={style.labels}>
<div>
<span className={style.label}>Title:</span>
<span className={style.content}>{data.title}</span>
</div>
<div>
<span className={style.label}>Presenter:</span>
<span className={style.content}>{data.presenter}</span>
</div>
<div>
<span className={style.label}>Subtitle:</span>
<span className={style.content}>{data.subtitle}</span>
</div>
<div>
<span className={style.label}>Note:</span>
<span className={style.content}>{data.note}</span>
</div>
</div>
)}
</div>
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { useInfoPanel } from '../../common/hooks/useSocket';
import InfoTitle from './CollapsableInfo';
import InfoLogger from './InfoLogger';
import InfoNif from './InfoNif';
import InfoTitle from './InfoTitle';
import style from './Info.module.scss';
@@ -31,7 +31,7 @@ export default function Info() {
return (
<>
<div className={style.main}>
<div className={style.panelHeader}>
<span>Ontime running on port 4001</span>
<span>{selected}</span>
</div>
+28 -56
View File
@@ -1,77 +1,49 @@
@use '../../theme/main' as *;
@use '../../theme/mixins' as *;
@use '../../theme/v2Styles' as *;
@mixin container {
@include second-container;
margin-top: 1em;
padding: 8px;
}
.container {
@include container;
}
.main {
font-size: 0.9em;
.panelHeader {
font-size: $inner-section-text-size;
font-family: $ontime-font-family;
color: $label-gray;
display: flex;
justify-content: space-between;
}
.collapsedTitle {
font-size: inherit;
padding-left: 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.container {
margin-top: $main-spacing;
}
.labelContainer {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.label,
.emptyLabel {
padding: 0;
margin: 0;
font-size: 0.9em;
.labels {
font-size: $inner-section-text-size;
display: flex;
flex-direction: column;
gap: $element-inner-spacing;
}
.label {
font-size: 0.9em;
color: $label-gray;
color: $section-white;
}
.label::after {
content: ': ';
}
.emptyLabel {
font-size: 0.8em;
color: $bg-gray-700;
}
.notes {
color: $notes-color;
overflow: hidden;
text-overflow: ellipsis;
.content {
color: $secondary-text-gray;
margin-left: $element-inner-spacing;
font-size: $text-body-size;
}
.interfaceList {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
gap: $element-spacing;
.interface {
@include action-link;
white-space: nowrap;
}
.linkIcon {
margin-left: 8px;
display: inline-block;
transform: rotate(45deg);
}
}
.interface {
font-size: 0.8em;
background-color: $bg-container-l3;
padding: 0 8px;
margin: 0 2px;
border-radius: 2px;
white-space: nowrap;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from '../../common/components/errorBoundary/ErrorBoundary';
import { handleLinks } from '../../common/utils/linkUtils';
@@ -11,7 +11,7 @@ import style from '../editors/Editor.module.scss';
export default function InfoExport() {
return (
<Box className={style.info} data-testid="panel-info">
<FiArrowUpRight className={style.corner} onClick={(event) => handleLinks(event, 'info')} />
<IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'info')} />
<div className={style.content}>
<ErrorBoundary>
<Info />
-147
View File
@@ -1,147 +0,0 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import { LoggingContext } from '../../common/context/LoggingContext';
import style from './InfoLogger.module.scss';
export default function InfoLogger() {
const { logData, clearLog } = useContext(LoggingContext);
const [data, setData] = useState([]);
const [collapsed, setCollapsed] = useState(false);
// Todo: save in local storage
const [showClient, setShowClient] = useState(true);
const [showServer, setShowServer] = useState(true);
const [showRx, setShowRx] = useState(true);
const [showTx, setShowTx] = useState(true);
const [showPlayback, setShowPlayback] = useState(true);
const [showUser, setShowUser] = useState(true);
useEffect(() => {
if (!logData) {
return;
}
const matchers = [];
if (showUser) {
matchers.push('USER');
}
if (showClient) {
matchers.push('CLIENT');
}
if (showServer) {
matchers.push('SERVER');
}
if (showRx) {
matchers.push('RX');
}
if (showTx) {
matchers.push('TX');
}
if (showPlayback) {
matchers.push('PLAYBACK');
}
const d = logData.filter((d) => matchers.some((m) => d.origin === m));
setData(d);
}, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]);
const disableOthers = useCallback((toEnable) => {
toEnable === 'USER' ? setShowUser(true) : setShowUser(false);
toEnable === 'CLIENT' ? setShowClient(true) : setShowClient(false);
toEnable === 'SERVER' ? setShowServer(true) : setShowServer(false);
toEnable === 'RX' ? setShowRx(true) : setShowRx(false);
toEnable === 'TX' ? setShowTx(true) : setShowTx(false);
toEnable === 'PLAYBACK' ? setShowPlayback(true) : setShowPlayback(false);
}, []);
return (
<div className={collapsed ? style.container : style.container__expanded}>
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)} />
{!collapsed && (
<>
<div className={style.toggleBar}>
<div
onClick={() => setShowUser((s) => !s)}
onAuxClick={() => disableOthers('USER')}
onContextMenu={(e) => e.preventDefault()}
className={showUser ? style.active : null}
role='button'
>
USER
</div>
<div
onClick={() => setShowClient((s) => !s)}
onAuxClick={() => disableOthers('CLIENT')}
onContextMenu={(e) => e.preventDefault()}
className={showClient ? style.active : null}
role='button'
>
CLIENT
</div>
<div
onClick={() => setShowServer((s) => !s)}
onAuxClick={() => disableOthers('SERVER')}
onContextMenu={(e) => e.preventDefault()}
className={showServer ? style.active : null}
role='button'
>
SERVER
</div>
<div
onClick={() => setShowPlayback((s) => !s)}
onAuxClick={() => disableOthers('PLAYBACK')}
onContextMenu={(e) => e.preventDefault()}
className={showPlayback ? style.active : null}
role='button'
>
Playback
</div>
<div
onClick={() => setShowRx((s) => !s)}
onAuxClick={() => disableOthers('RX')}
onContextMenu={(e) => e.preventDefault()}
className={showRx ? style.active : null}
role='button'
>
RX
</div>
<div
onClick={() => setShowTx((s) => !s)}
onAuxClick={() => disableOthers('TX')}
onContextMenu={(e) => e.preventDefault()}
className={showTx ? style.active : null}
role='button'
>
TX
</div>
<div onClick={clearLog} className={style.clear} role='button'>
Clear
</div>
</div>
<ul className={style.log}>
{data.map((d) => (
<li
key={d.id}
className={
d.level === 'INFO'
? style.info
: d.level === 'WARN'
? style.warn
: d.level === 'ERROR'
? style.error
: ''
}
>
<div className={style.time}>{d.time}</div>
<div className={style.origin}>{d.origin}</div>
<div className={style.msg}>{d.text}</div>
</li>
))}
</ul>
</>
)}
</div>
);
}
+37 -62
View File
@@ -1,16 +1,17 @@
@use 'Info.module' as *;
@use '../../theme/main' as *;
@use '../../theme/v2Styles' as *;
@use '../../theme/mixins' as *;
.container,
.container__expanded{
@include container;
max-height: 80%;
}
$info-gray: $secondary-text-gray;
$info-hover: $section-white;
.container__expanded {
min-height: 50%;
height: 100%
.infoLoggerContainer {
max-height: 80%;
margin-top: 32px;
&.expanded {
min-height: 50%;
height: 100%
}
}
.log {
@@ -18,70 +19,44 @@
overflow-y: scroll;
font-size: 0.8em;
user-select: text;
padding: 0 0.5em;
margin: 0 0.5em;
}
li {
display: flex;
margin-bottom: 2px;
.time {
width: 4.5em
}
.origin {
width: 6em;
}
.msg {
flex: 1;
}
}
li.info {
.logEntry {
display: flex;
margin-bottom: 2px;
&.INFO {
color: $info-gray;
}
li.warn {
&.WARN {
color: $warning-orange;
}
li.error {
&.ERROR {
color: $error-red;
}
li:hover {
color: $info-gray-hover;
&:hover {
color: $info-hover;
}
.time {
width: 4.5em
}
.origin {
width: 6em;
}
.msg {
flex: 1;
}
}
.info {
color: $text-white;
}
.error {
color: red;
}
.toggleBar {
.buttonBar {
display: flex;
flex-wrap: wrap;
gap: 4px;
font-size: 0.7em;
gap: 8px;
justify-content: flex-start;
padding: 0.5em 8px;
font-weight: 600;
div {
padding: 2px 8px;
background: #0002;
border: 1px solid #fff1;
border-radius: 2px;
cursor: pointer;
}
div.active {
background: $ontime-roll;
color: white;
}
.clear {
border: 1px solid rgba($ontime-pink, 0.5);
}
margin-bottom: 8px;
}
+148
View File
@@ -0,0 +1,148 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { Button } from '@chakra-ui/react';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import { Log, LoggingContext } from '../../common/context/LoggingContext';
import style from './InfoLogger.module.scss';
enum LOG_FILTER {
USER = 'USER',
CLIENT = 'CLIENT',
SERVER = 'SERVER',
RX = 'RX',
TX = 'TX',
PLAYBACK = 'PLAYBACK',
}
export default function InfoLogger() {
const { logData, clearLog } = useContext(LoggingContext);
const [data, setData] = useState<Log[]>([]);
const [collapsed, setCollapsed] = useState(false);
const [showClient, setShowClient] = useState(true);
const [showServer, setShowServer] = useState(true);
const [showRx, setShowRx] = useState(true);
const [showTx, setShowTx] = useState(true);
const [showPlayback, setShowPlayback] = useState(true);
const [showUser, setShowUser] = useState(true);
useEffect(() => {
if (!logData) {
return;
}
const matchers: LOG_FILTER[] = [];
if (showUser) {
matchers.push(LOG_FILTER.USER);
}
if (showClient) {
matchers.push(LOG_FILTER.CLIENT);
}
if (showServer) {
matchers.push(LOG_FILTER.SERVER);
}
if (showRx) {
matchers.push(LOG_FILTER.RX);
}
if (showTx) {
matchers.push(LOG_FILTER.TX);
}
if (showPlayback) {
matchers.push(LOG_FILTER.PLAYBACK);
}
const filteredData = logData.filter((entry) => matchers.some((match) => entry.origin === match));
setData(filteredData);
}, [logData, showUser, showClient, showServer, showPlayback, showRx, showTx]);
const disableOthers = useCallback((toEnable: LOG_FILTER) => {
toEnable === LOG_FILTER.USER ? setShowUser(true) : setShowUser(false);
toEnable === LOG_FILTER.CLIENT ? setShowClient(true) : setShowClient(false);
toEnable === LOG_FILTER.SERVER ? setShowServer(true) : setShowServer(false);
toEnable === LOG_FILTER.RX ? setShowRx(true) : setShowRx(false);
toEnable === LOG_FILTER.TX ? setShowTx(true) : setShowTx(false);
toEnable === LOG_FILTER.PLAYBACK ? setShowPlayback(true) : setShowPlayback(false);
}, []);
return (
<div className={`${style.infoLoggerContainer} ${collapsed? '' : style.expanded}`}>
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((prev) => !prev)} />
{!collapsed && (
<>
<div className={style.buttonBar}>
<Button
variant={showUser ? 'ontime-filled' : 'ontime-outlined'}
size='xs'
onClick={() => setShowUser((s) => !s)}
onAuxClick={() => disableOthers(LOG_FILTER.USER)}
onContextMenu={(e) => e.preventDefault()}
>
USER
</Button>
<Button
variant={showClient ? 'ontime-filled' : 'ontime-outlined'}
size='xs'
onClick={() => setShowClient((s) => !s)}
onAuxClick={() => disableOthers(LOG_FILTER.CLIENT)}
onContextMenu={(e) => e.preventDefault()}
>
CLIENT
</Button>
<Button
variant={showServer ? 'ontime-filled' : 'ontime-outlined'}
size='xs'
onClick={() => setShowServer((s) => !s)}
onAuxClick={() => disableOthers(LOG_FILTER.SERVER)}
onContextMenu={(e) => e.preventDefault()}
>
SERVER
</Button>
<Button
variant={showPlayback ? 'ontime-filled' : 'ontime-outlined'}
size='xs'
onClick={() => setShowPlayback((s) => !s)}
onAuxClick={() => disableOthers(LOG_FILTER.PLAYBACK)}
onContextMenu={(e) => e.preventDefault()}
>
PLAYBACK
</Button>
<Button
variant={showRx ? 'ontime-filled' : 'ontime-outlined'}
size='xs'
onClick={() => setShowRx((s) => !s)}
onAuxClick={() => disableOthers(LOG_FILTER.RX)}
onContextMenu={(e) => e.preventDefault()}
>
RX
</Button>
<Button
variant={showTx ? 'ontime-filled' : 'ontime-outlined'}
size='xs'
onClick={() => setShowTx((s) => !s)}
onAuxClick={() => disableOthers(LOG_FILTER.TX)}
onContextMenu={(e) => e.preventDefault()}
>
TX
</Button>
<Button
variant='ontime-subtle'
size='xs'
onClick={clearLog}
>
Clear
</Button>
</div>
<ul className={style.log}>
{data.map((logEntry) => (
<li key={logEntry.id} className={`${style.logEntry} ${style[logEntry.level]} `}>
<span className={style.time}>{logEntry.time}</span>
<span className={style.origin}>{logEntry.origin}</span>
<span className={style.msg}>{logEntry.text}</span>
</li>
))}
</ul>
</>
)}
</div>
);
}
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import useInfo from '../../common/hooks-query/useInfo';
@@ -7,28 +8,32 @@ import { openLink } from '../../common/utils/linkUtils';
import style from './Info.module.scss';
export default function InfoNif() {
const { data, status } = useInfo();
const { data } = useInfo();
const [collapsed, setCollapsed] = useState(false);
const baseURL = 'http://__IP__:4001';
const handleClick = (address: string) => {
const baseURL = 'http://__IP__:4001';
openLink(baseURL.replace('__IP__', address));
};
return (
<div className={style.container}>
<CollapseBar
title='Network Info'
isCollapsed={collapsed}
onClick={() => setCollapsed((c) => !c)}
onClick={() => setCollapsed((prev) => !prev)}
/>
{!collapsed && (status === 'success') &&(
{!collapsed && (
<div className={style.interfaceList}>
{data?.networkInterfaces.map((e) => (
<a
key={e.address}
href='#!'
onClick={() => openLink(baseURL.replace('__IP__', e.address))}
{data?.networkInterfaces.map((nif) => (
<span
key={nif.address}
onClick={() => handleClick(nif.address)}
className={style.interface}
>
{`${e.name} - ${e.address}`}
</a>
{`${nif.name} - ${nif.address}`}
<IoArrowUp className={style.linkIcon} />
</span>
))}
</div>
)}
-52
View File
@@ -1,52 +0,0 @@
import { useState } from 'react';
import PropTypes from 'prop-types';
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 noTitl = data.title == null || data.title === '';
const noPres = data.presenter == null || data.presenter === '';
const noSubt = data.subtitle == null || data.subtitle === '';
const noNote = data.note == null || data.note === '';
return (
<div className={style.container}>
<CollapseBar
title={title}
isCollapsed={collapsed}
onClick={() => setCollapsed((c) => !c)}
/>
{!collapsed && (
<>
<div className={style.labelContainer}>
<span className={noTitl ? style.emptyLabel : style.label}>Title</span>
{data.title}
</div>
<div className={style.labelContainer}>
<span className={noPres ? style.emptyLabel : style.label}>Presenter</span>
{data.presenter}
</div>
<div className={style.labelContainer}>
<span className={noSubt ? style.emptyLabel : style.label}>Subtitle</span>
{data.subtitle}
</div>
<div className={style.notes}>
<span className={noNote ? style.emptyLabel : style.label}>Note</span>
{data.note}
</div>
</>
)}
</div>
);
}
InfoTitle.propTypes = {
title: PropTypes.string,
data: PropTypes.object,
roll: PropTypes.bool,
}
@@ -1,20 +0,0 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { queryClientMock } from '../../../__mocks__/QueryClient.mock';
import Info from '../Info';
test('check static info render', async () => {
// need to inject the socket provider to make component
// render without failing
render(
<QueryClientProvider client={queryClientMock}>
<Info />
</QueryClientProvider>,
);
// Info titles
// substring match, ignore case
expect(screen.getByText(/running/i)).toBeInTheDocument();
expect(screen.getByText(/event/i)).toBeInTheDocument();
});
+17 -13
View File
@@ -1,10 +1,10 @@
import { useCallback, useEffect } from 'react';
import { VStack } from '@chakra-ui/react';
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 { FiUpload } from '@react-icons/all-files/fi/FiUpload';
import { IoScan } from '@react-icons/all-files/io5/IoScan';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from 'common/api/ontimeApi';
@@ -28,6 +28,12 @@ const buttonStyle = {
fontSize: '1.5em',
size: 'lg',
colorScheme: 'white',
_hover: {
background: 'rgba(255, 255, 255, 0.10)' // $white-10
},
_active: {
background: 'rgba(255, 255, 255, 0.13)' // $white-13
}
};
export default function MenuBar(props: MenuBarProps) {
@@ -94,17 +100,17 @@ export default function MenuBar(props: MenuBarProps) {
<QuitIconBtn clickHandler={() => actionHandler('shutdown')} />
<TooltipActionBtn
{...buttonStyle}
icon={<FiMaximize />}
icon={<IoScan />}
clickHandler={() => actionHandler('max')}
tooltip='Show full window'
aria-label=''
aria-label='Show full window'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiMinimize />}
clickHandler={() => actionHandler('min')}
tooltip='Close to tray'
aria-label=''
tooltip='Minimise to tray'
aria-label='Minimise to tray'
/>
<div className={style.gap} />
<TooltipActionBtn
@@ -112,7 +118,7 @@ export default function MenuBar(props: MenuBarProps) {
icon={<FiHelpCircle />}
clickHandler={() => actionHandler('help')}
tooltip='Help'
aria-label=''
aria-label='Help'
/>
<TooltipActionBtn
{...buttonStyle}
@@ -120,8 +126,7 @@ export default function MenuBar(props: MenuBarProps) {
className={isSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
isRound
aria-label=''
aria-label='Settings'
/>
<div className={style.gap} />
<TooltipActionBtn
@@ -129,16 +134,15 @@ export default function MenuBar(props: MenuBarProps) {
icon={<FiUpload />}
className={isUploadOpen ? style.open : ''}
clickHandler={onUploadOpen}
tooltip='Upload event list'
isRound
aria-label=''
tooltip='Upload showfile'
aria-label='Upload showfile'
/>
<TooltipActionBtn
{...buttonStyle}
icon={<FiSave />}
clickHandler={downloadRundown}
tooltip='Export event list'
aria-label=''
tooltip='Export showfile'
aria-label='Export showfile'
/>
</VStack>
);
+22 -30
View File
@@ -1,30 +1,24 @@
import { memo, useCallback, useContext } from 'react';
import {
Divider,
Button,
HStack,
IconButton,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Switch,
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 { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { CursorContext } from 'common/context/CursorContext';
import { useEventAction } from '../../common/hooks/useEventAction';
import style from './RundownMenu.module.scss';
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
const RundownMenu = () => {
const { isCursorLocked, toggleCursorLocked } = useContext(CursorContext);
const { addEvent, deleteAllEvents } = useEventAction();
@@ -56,33 +50,31 @@ const RundownMenu = () => {
<Switch
defaultChecked={isCursorLocked}
onChange={(event) => toggleCursorLocked(event.target.checked)}
colorScheme='blue'
variant='ontime'
/>
Lock cursor to current
</label>
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add / Delete ...'>
<MenuButton
as={IconButton}
aria-label='Create Menu'
size='sm'
icon={<FiPlus />}
colorScheme='white'
variant='outline'
/>
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => eventAction('event')}>
Add Event at start
<Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<MenuButton
as={Button}
leftIcon={<IoAdd />}
size='sm'
variant='ontime-subtle'
>
Add event
</MenuButton>
<MenuList>
<MenuItem icon={<IoAdd />} onClick={() => eventAction('event')}>
Event at start
</MenuItem>
<MenuItem icon={<FiClock />} onClick={() => eventAction('delay')}>
Add Delay at start
<MenuItem icon={<IoTimerOutline />} onClick={() => eventAction('delay')}>
Delay at start
</MenuItem>
<MenuItem icon={<FiMinusCircle />} onClick={() => eventAction('block')}>
Add Block at start
Block at start
</MenuItem>
<Divider />
<MenuItem icon={<FiTrash2 />} onClick={() => eventAction('delete-all')} color='red.500'>
<MenuDivider />
<MenuItem icon={<FiTrash2 />} onClick={() => eventAction('delete-all')} color='#D20300'>
Delete All
</MenuItem>
</MenuList>
@@ -1,33 +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 MenuBar from '../MenuBar';
const onSettingOpenHandler = vi.fn();
const onSettingsCloseHandler = vi.fn();
const onUploadOpenHandler = vi.fn();
const isOpen = false;
const renderInMock = () => {
render(
<QueryClientProvider client={queryClientMock}>
<MenuBar
isSettingsOpen={isOpen}
onSettingsOpen={onSettingOpenHandler}
onSettingsClose={onSettingsCloseHandler}
isUploadOpen={isOpen}
onUploadOpen={onUploadOpenHandler}
/>
</QueryClientProvider>
);
};
test('check that menu bar renders correctly', () => {
// need to inject the react query provider
renderInMock();
const nButtons = screen.getAllByRole('button').length;
expect(nButtons).toBe(7);
});
+12 -25
View File
@@ -1,4 +1,4 @@
import { createRef, useCallback, useContext, useEffect } from 'react';
import { createRef, Fragment, useCallback, useContext, useEffect } from 'react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Button } from '@chakra-ui/react';
import {
@@ -14,8 +14,6 @@ import { duplicateEvent } from 'common/utils/eventsManager';
import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types';
import useSubscription from '../../common/hooks/useSubscription';
import QuickAddBlock from './quick-add-block/QuickAddBlock';
import RundownEntry from './RundownEntry';
@@ -32,8 +30,6 @@ export default function Rundown(props) {
const { addEvent, reorderEvent } = useEventAction();
const cursorRef = createRef();
const showQuickEntry = useAtomValue(showQuickEntryAtom);
const [selectedId] = useSubscription('selected-id', null);
const [nextId] = useSubscription('next-id', null);
const insertAtCursor = useCallback(
(type, cursor) => {
@@ -83,20 +79,17 @@ export default function Rundown(props) {
if (event.keyCode === 38) {
if (cursor > 0) moveCursorUp();
}
// E
if (event.code === "KeyE") {
if (event.code === 'KeyE') {
event.preventDefault();
if (cursor == null) return;
insertAtCursor('event', cursor);
}
// D
if (event.code === "KeyD") {
if (event.code === 'KeyD') {
event.preventDefault();
if (cursor == null) return;
insertAtCursor('delay', cursor);
}
// B
if (event.code === "KeyB") {
if (event.code === 'KeyB') {
event.preventDefault();
if (cursor == null) return;
insertAtCursor('block', cursor);
@@ -166,7 +159,7 @@ export default function Rundown(props) {
[reorderEvent],
);
if (entries.length < 1) {
if (!entries.length) {
return (
<div className={style.alignCenter}>
<Empty text='No data yet' style={{ marginTop: '7vh' }} />
@@ -209,25 +202,19 @@ export default function Rundown(props) {
}
const isLast = index === entries.length - 1;
return (
<div
key={entry.id}
className={`${style.bgElement}
${entry.type === 'event' && cumulativeDelay !== 0 ? style.delayed : ''}`}
>
<div
ref={cursor === index ? cursorRef : undefined}
className={cursor === index ? style.cursor : ''}
>
<Fragment key={entry.id}>
<div ref={cursor === index ? cursorRef : undefined}>
<RundownEntry
type={entry.type}
index={index}
eventIndex={eventIndex}
data={entry}
selected={selectedId === entry.id}
next={nextId === entry.id}
selected={data.selectedEventId === entry.id}
hasCursor={cursor === index}
next={data.nextEventId === entry.id}
delay={cumulativeDelay}
previousEnd={previousEnd}
playback={selectedId === entry.id ? data.playback : undefined}
playback={data.selectedEventId === entry.id ? data.playback : undefined}
/>
</div>
{((showQuickEntry && index === cursor) || isLast) && (
@@ -239,7 +226,7 @@ export default function Rundown(props) {
disableAddBlock={entry.type === 'block'}
/>
)}
</div>
</Fragment>
);
})}
{provided.placeholder}
@@ -20,17 +20,6 @@
align-self: center;
}
.cursor {
border-radius: 3px;
outline: 1px solid $action-blue;
}
.bgElement {
&.delayed {
background: rgba($block-delay-color, 0.3);
}
}
.alignCenter {
text-align: center;
flex-direction: column;
+4 -2
View File
@@ -32,6 +32,7 @@ interface RundownEntryProps {
eventIndex: number;
data: OntimeRundownEntry;
selected: boolean;
hasCursor: boolean;
next: boolean;
delay: number;
previousEnd: number;
@@ -39,7 +40,7 @@ interface RundownEntryProps {
}
export default function RundownEntry(props: RundownEntryProps) {
const { index, eventIndex, data, selected, next, delay, previousEnd, playback } = props;
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, playback } = props;
const { emitError } = useContext(LoggingContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
@@ -53,7 +54,7 @@ export default function RundownEntry(props: RundownEntryProps) {
value: unknown;
}
const actionHandler = useCallback(
(action: EventItemActions, payload: number | FieldValue) => {
(action: EventItemActions, payload?: number | FieldValue) => {
switch (action) {
case 'set-cursor': {
moveCursorTo(payload as number);
@@ -151,6 +152,7 @@ export default function RundownEntry(props: RundownEntryProps) {
next={next}
skip={data.skip}
selected={selected}
hasCursor={hasCursor}
playback={playback}
actionHandler={actionHandler}
/>
@@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react';
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { CursorProvider } from 'common/context/CursorContext';
import { handleLinks } from 'common/utils/linkUtils';
@@ -12,7 +12,7 @@ export default function RundownExport() {
return (
<CursorProvider>
<Box className={style.editor} data-testid='panel-rundown'>
<FiArrowUpRight
<IoArrowUp
className={style.corner}
onClick={(event) => handleLinks(event, 'rundown')}
/>
+21 -19
View File
@@ -1,31 +1,33 @@
@use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *;
$block-gap: 4px;
$element-spacing: 4px;
$binder-width: 32px;
$clearance: 8px;
$block-border-radius: 3px;
$block-element-spacing: 4px;
$block-binder-width: 32px;
$block-clearance: 8px;
$block-border-radius: 8px;
$block-text-color: $gray-50;
$block-bg: $gray-1200;
$block-box-shadow: rgba(0, 0, 0, 0.5) 0 0 3px 2px;
$secondary-block-height: 40px;
@mixin block-styling() {
box-sizing: content-box;
background-color: $block-bg;
border: 1px solid #383838;
box-shadow: $block-box-shadow;
font-family: $ontime-font-family;
border-radius: $block-border-radius;
}
@mixin block-spacing() {
padding: 4px 10px 4px 2px;
gap: 2px;
}
@mixin action-overlay() {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-content: center;
opacity: 0.6;
transition: linear 0.1s;
}
@mixin block-hover() {
opacity: 1;
transition: linear 0.1s;
}
@mixin drag-style() {
font-size: 20px;
text-align: center;
justify-self: center;
opacity: 0.3;
cursor: grab;
transition: opacity 0.3s;
@@ -1,44 +0,0 @@
import { Draggable } from 'react-beautiful-dnd';
import { HStack } from '@chakra-ui/react';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import PropTypes from 'prop-types';
import ActionButtons from '../../../common/components/buttons/ActionButtons';
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
import style from './BlockBlock.module.scss';
export default function BlockBlock(props) {
const { index, data, actionHandler } = props;
return (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
<div className={style.block} {...provided.draggableProps} ref={provided.innerRef}>
<span className={style.drag} {...provided.dragHandleProps}>
<IoReorderTwo />
</span>
<HStack spacing='4px' className={style.actionOverlay}>
<TooltipLoadingActionBtn
clickHandler={() => actionHandler('delete')}
icon={<IoRemove />}
tooltip='Delete'
variant='outline'
colorScheme='white'
size='sm'
aria-label='Delete'
/>
<ActionButtons showAdd showDelay actionHandler={actionHandler} />
</HStack>
</div>
)}
</Draggable>
);
}
BlockBlock.propTypes = {
index: PropTypes.number.isRequired,
data: PropTypes.object.isRequired,
actionHandler: PropTypes.func.isRequired,
};
@@ -1,39 +1,20 @@
@use '../../../theme/main' as *;
@use '../blockMixins' as *;
/* ============= COMMON ============= */
.block {
@include block-spacing;
@include block-styling;
box-sizing: content-box;
display: grid;
grid-template-columns: 40px 1fr;
grid-template-columns: 32px 1fr;
align-items: center;
height: 40px;
border-radius: 2px;
border: $border-l3;
border-bottom: 4px solid $block-block-color;
background-color: $bg-container-l2;
height: $secondary-block-height;
}
/* ================ DRAG ================ */
.drag {
@include drag-style;
color: $block-block-color;
}
/* ============== ACTION ================ */
.actionOverlay {
@include action-overlay;
justify-self: end;
}
.block:hover > .actionOverlay {
@include block-hover;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
justify-self: flex-end;
}
@@ -0,0 +1,37 @@
import { Draggable } from 'react-beautiful-dnd';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { OntimeBlock, OntimeEvent } from '../../../common/models/EventTypes';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry';
import style from './BlockBlock.module.scss';
interface BlockBlockProps {
index: number;
data: OntimeBlock;
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void;
}
export default function BlockBlock(props: BlockBlockProps) {
const { index, data, actionHandler } = props;
return (
<Draggable key={data.id} draggableId={data.id} index={index}>
{(provided) => (
<div className={style.block} {...provided.draggableProps} ref={provided.innerRef}>
<span className={style.drag} {...provided.dragHandleProps}>
<IoReorderTwo />
</span>
<BlockActionMenu
className={style.actionOverlay}
showAdd
showDelay
enableDelete
actionHandler={actionHandler}
/>
</div>
)}
</Draggable>
);
}
@@ -1,49 +1,18 @@
@use '../../../theme/main' as *;
@use '../blockMixins' as *;
/* ============= COMMON ============= */
.delay {
@include block-spacing;
@include block-styling;
box-sizing: content-box;
display: grid;
grid-template-columns: 32px 1fr auto;
grid-template-areas: 'drag inpt btns';
align-items: center;
height: 40px;
border-radius: 2px;
border: $border-l3;
border-top: 4px solid $block-delay-color;
background-color: $bg-container-l2;
height: $secondary-block-height;
gap: 8px;
}
/* ================ DRAG ================ */
.drag {
grid-area: drag;
@include drag-style;
color: $block-delay-color;
}
/* =============== INPUT ================ */
.input {
grid-area: inpt;
}
/* ============== ACTION ================ */
.actionOverlay {
@include action-overlay;
}
.delay:hover > .actionOverlay {
@include block-hover;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
grid-area: drag;
}
@@ -1,32 +1,34 @@
import { useCallback } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { Button, HStack } from '@chakra-ui/react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import ActionButtons from 'common/components/buttons/ActionButtons';
import TooltipLoadingActionBtn from 'common/components/buttons/TooltipLoadingActionBtn';
import DelayInput from 'common/components/input/DelayInput';
import DelayInput from 'common/components/input/delay-input/DelayInput';
import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import { OntimeDelay, OntimeEvent } from '../../../common/models/EventTypes';
import BlockActionMenu from '../event-block/composite/BlockActionMenu';
import { EventItemActions } from '../RundownEntry';
import style from './DelayBlock.module.scss';
export default function DelayBlock(props) {
interface DelayBlockProps {
data: OntimeDelay,
index: number;
actionHandler: (action: EventItemActions, payload?: number | { field: keyof OntimeEvent, value: unknown }) => void;
}
export default function DelayBlock(props: DelayBlockProps) {
const { data, index, actionHandler } = props;
const { applyDelay, deleteEvent, updateEvent } = useEventAction();
const { applyDelay, updateEvent } = useEventAction();
const applyDelayHandler = useCallback(() => {
applyDelay(data.id);
}, [data.id, applyDelay]);
const deleteHandler = useCallback(() => {
deleteEvent(data.id);
}, [data.id, deleteEvent]);
const delaySubmitHandler = useCallback(
(value) => {
(value: number) => {
const newEvent = {
id: data.id,
duration: value * 60000,
@@ -34,7 +36,7 @@ export default function DelayBlock(props) {
updateEvent(newEvent);
},
[data.id, updateEvent]
[data.id, updateEvent],
);
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
@@ -47,39 +49,22 @@ export default function DelayBlock(props) {
<IoReorderTwo />
</span>
<DelayInput
className={style.input}
value={delayValue}
submitHandler={delaySubmitHandler}
/>
<HStack spacing='4px' className={style.actionOverlay}>
<HStack spacing='8px' className={style.actionOverlay}>
<Button
onClick={applyDelayHandler}
size='sm'
color="#F57C13"
borderColor="#F57C13"
leftIcon={<FiCheck />}
variant='outline'
leftIcon={<IoCheckmark />}
variant='ontime-subtle-white'
>
Apply delay
</Button>
<TooltipLoadingActionBtn
clickHandler={deleteHandler}
icon={<IoRemove />}
tooltip='Delete'
variant='outline'
colorScheme='white'
size='sm'
/>
<ActionButtons showAdd actionHandler={actionHandler} />
<BlockActionMenu showAdd enableDelete actionHandler={actionHandler} />
</HStack>
</div>
)}
</Draggable>
);
}
DelayBlock.propTypes = {
data: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
actionHandler: PropTypes.func.isRequired,
};
@@ -1,6 +1,130 @@
@use '../../../theme/main' as *;
@use '../../../theme/v2Styles' as *;
@use '../blockMixins' as *;
.eventBlock {
@include block-styling;
display: grid;
grid-template-areas:
"binder ... ... ..."
"binder pb-actions times actions"
"binder pb-actions title title"
"binder pb-actions estatus estatus"
"binder ... ... ...";
grid-template-columns: $block-binder-width auto 1fr auto;
grid-template-rows: 4px 36px 36px 36px 4px;
align-items: center;
margin: 4px 2px;
padding-right: $block-clearance;
gap: 2px;
&.selected {
background-color: #101010;
}
&.hasCursor {
outline: 1px solid #779BE7;
}
&.skip {
border: 1px solid rgba(white, 0.03);
box-shadow: none;
.delayNote,
.eventTitle,
.eventNote,
.binder,
.eventTimers,
.eventStatus {
opacity: $opacity-disabled;
}
}
}
.binder {
grid-area: binder;
height: 100%;
display: grid;
place-content: center;
position: relative;
cursor: pointer;
border-radius: $block-border-radius 0 0 $block-border-radius;
background-color: #303030; // to override inline
color: $section-white;
font-size: 17px;
.drag {
@include drag-style;
position: absolute;
margin-top: 4px;
}
}
.playbackActions {
grid-area: pb-actions;
display: flex;
flex-direction: column;
margin: 0 8px;
gap: 6px;
}
.eventTimers {
grid-area: times;
display: flex;
align-items: center;
gap: $block-clearance;
height: 100%;
.delayNote {
font-size: 12px;
line-height: 14px;
color: #E69056;
}
}
.eventTitle {
grid-area: title;
display: block;
font-size: 18px;
&.noTitle {
.preview {
opacity: $opacity-disabled;
}
}
}
.eventActions {
grid-area: actions;
display: flex;
gap: $block-clearance;
justify-content: flex-end;
}
.eventOptions {
margin: $element-spacing 16px $element-spacing 0;
}
.progressBg {
grid-area: progb;
border-radius: 2px;
background-color: #282828;
opacity: 1;
height: 100%;
}
.progressBg.hidden {
opacity: 0;
}
.flip {
transform: rotateY(180deg);
}
.statusElements {
grid-area: estatus;
display: grid;
@@ -9,189 +133,40 @@
"progb progb";
gap: 2px;
grid-template-rows: auto 4px;
.eventNote {
grid-area: notes;
display: block;
font-size: 15px;
color: $notes-color;
line-height: 15px;
}
.eventStatus {
grid-area: status;
display: flex;
justify-content: flex-end;
gap: $element-spacing;
.statusIcon {
width: 24px;
height: 24px;
border-radius: 12px;
padding: 4px;
color: $text-gray-disabled;
}
.statusIcon.statusNext {
border: 1px solid transparent;
&.enabled {
color: $text-white;
background-color: $ontime-accent;
}
}
.statusIcon.statusDelay {
&.enabled {
color: $text-white;
background-color: $ontime-delay;
}
}
.statusIcon.statusPublic {
&.enabled {
color: $text-white;
background-color: $ontime-roll;
}
}
}
}
.eventBlock {
// layout
display: grid;
grid-template-areas:
"binder pb-actions times actions"
"binder pb-actions title title"
"binder pb-actions estatus estatus";
grid-template-columns: $binder-width auto 1fr auto;
grid-template-rows: 36px 36px 36px;
align-items: center;
margin: 4px 2px;
padding-right: $clearance;
// style - general
border-radius: $block-border-radius;
gap: 2px;
// style - colour
background-color: $bg-container-l2;
border: $border-l3;
// variant
&.selected {
background-color: $bg-container-l3;
}
&.skip {
background-color: $bg-gray-1000;
border: 1px solid transparent;
.eventTimers > .delayNote,
.eventTitle,
.eventNote {
color: $text-gray-disabled;
}
.binder,
.eventTimers,
.eventStatus {
opacity: $opacity-disabled;
}
}
.binder {
// layout
grid-area: binder;
height: 100%;
display: grid;
place-content: center;
position: relative;
cursor: pointer;
// style - general
border-radius: 3px 0 0 3px;
border-right: 1px solid $bg-gray-900;
// style - colour
background-color: $bg-container-l3; // override inline
color: $text-white;
.drag {
@include drag-style;
position: absolute;
}
}
.playbackActions {
// layout
grid-area: pb-actions;
display: flex;
flex-direction: column;
margin: $element-spacing $clearance $element-spacing $element-spacing;
// style - general
gap: 6px;
}
.eventTimers {
grid-area: times;
display: flex;
align-items: center;
gap: $clearance;
height: 100%;
.delayNote {
font-size: 11px;
color: $text-delay;
}
}
.eventTitle {
grid-area: title;
display: block;
font-size: 18px;
.eventTitle__preview {
width: 100%;
}
&.noTitle {
span {
opacity: $opacity-disabled;
}
}
input {
border-radius: 2px;
border: 1px;
}
}
.eventActions {
grid-area: actions;
display: flex;
gap: $element-spacing;
justify-content: flex-end;
}
.eventOptions {
margin: $element-spacing 16px $element-spacing 0;
}
}
.progressBg {
grid-area: progb;
border-radius: 1px;
background-color: $bg-container-l1;
opacity: 1;
// layout
height: 100%;
padding: 2px 0;
}
.progressBg.hidden {
opacity: 0;
}
.eventNote {
grid-area: notes;
display: block;
font-size: 13px;
color: $block-text-color;
line-height: 13px;
}
.eventStatus {
grid-area: status;
display: flex;
justify-content: flex-end;
gap: 8px;
.statusIcon {
width: 16px;
height: 16px;
color: #404040;
}
.statusNext.enabled {
color: $playback-start;
}
.statusDelay.enabled {
color: $ontime-delay;
}
.statusPublic.enabled {
color: $action-blue;
}
}
@@ -4,17 +4,16 @@ import { Editable, EditableInput, EditablePreview, Tooltip } from '@chakra-ui/re
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
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 { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { IoReload } from '@react-icons/all-files/io5/IoReload';
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 { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { editorEventId } from 'common/atoms/LocalEventSettings';
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn';
import { getAccessibleColour } from 'common/utils/styleUtils';
import { cx, getAccessibleColour } from 'common/utils/styleUtils';
import { useAtom } from 'jotai';
import { useEventAction } from '../../../common/hooks/useEventAction';
@@ -23,7 +22,7 @@ import { Playstate } from '../../../common/models/OntimeTypes';
import { tooltipDelayMid } from '../../../ontimeConfig';
import { EventItemActions } from '../RundownEntry';
import EventBlockActionMenu from './composite/EventBlockActionMenu';
import BlockActionMenu from './composite/BlockActionMenu';
import EventBlockProgressBar from './composite/EventBlockProgressBar';
import EventBlockTimers from './composite/EventBlockTimers';
@@ -31,9 +30,6 @@ import style from './EventBlock.module.scss';
const blockBtnStyle = {
size: 'sm',
colorScheme: 'white',
variant: 'ghost',
fontSize: '20px',
};
const tooltipProps = {
@@ -56,8 +52,9 @@ interface EventBlockProps {
next: boolean;
skip: boolean;
selected: boolean;
hasCursor: boolean;
playback?: Playstate;
actionHandler: (action: EventItemActions, payload: any) => void;
actionHandler: (action: EventItemActions, payload?: any) => void;
}
export default function EventBlock(props: EventBlockProps) {
@@ -77,6 +74,7 @@ export default function EventBlock(props: EventBlockProps) {
next,
skip = false,
selected,
hasCursor,
playback,
actionHandler,
} = props;
@@ -89,6 +87,7 @@ export default function EventBlock(props: EventBlockProps) {
const hasDelay = delay !== 0 && delay !== null;
// Todo: could I re-render the item without causing a state change here?
// ?? use refs instead?
useEffect(() => {
setBlockTitle(title);
}, [title]);
@@ -115,13 +114,18 @@ export default function EventBlock(props: EventBlockProps) {
playBtnStyles._hover = {};
}
const blockClasses = cx([
style.eventBlock,
skip ? style.skip : null,
selected ? style.selected : null,
hasCursor ? style.hasCursor : null,
]);
return (
<Draggable key={eventId} draggableId={eventId} index={index}>
{(provided) => (
<div
className={`${style.eventBlock} ${skip ? style.skip : ''} ${
selected ? style.selected : ''
}`}
className={blockClasses}
{...provided.draggableProps}
ref={provided.innerRef}
>
@@ -138,36 +142,38 @@ export default function EventBlock(props: EventBlockProps) {
</div>
<div className={style.playbackActions}>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Skip event'
tooltip='Skip event'
openDelay={tooltipDelayMid}
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
{...blockBtnStyle}
variant={skip ? 'solid' : 'ghost'}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
tabIndex={-1}
disabled={selected}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Load event'
tooltip='Load event'
openDelay={tooltipDelayMid}
icon={selected ? <IoPlayBackOutline /> : <IoReload />}
icon={<IoReload className={style.flip} />}
disabled={skip}
{...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)}
tabIndex={-1}
/>
<TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Start event'
tooltip='Start event'
openDelay={tooltipDelayMid}
icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />}
disabled={skip}
{...blockBtnStyle}
variant={eventIsPlaying ? 'solid' : 'ghost'}
clickHandler={() => setEventPlayback.startEvent(eventId)}
backgroundColor={eventIsPlaying ? '#58A151' : undefined}
_hover={{backgroundColor: eventIsPlaying ? '#58A151' : undefined}}
tabIndex={-1}
/>
</div>
@@ -186,7 +192,7 @@ export default function EventBlock(props: EventBlockProps) {
onChange={(value) => setBlockTitle(value)}
onSubmit={(value) => handleTitle(value)}
>
<EditablePreview className={style.eventTitle__preview} />
<EditablePreview className={style.preview} />
<EditableInput />
</Editable>
<div className={style.statusElements}>
@@ -195,54 +201,53 @@ export default function EventBlock(props: EventBlockProps) {
<EventBlockProgressBar playback={playback} />
</div>
<div className={style.eventStatus}>
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}>
<span
className={`${style.statusIcon} ${style.statusNext} ${next ? style.enabled : ''}`}
<Tooltip
label='Next event'
isDisabled={!next}
shouldWrapChildren {...tooltipProps}
>
<IoReturnDownForward />
</span>
<IoPlaySkipForward
className={`${style.statusIcon} ${style.statusNext} ${next ? style.enabled : ''}`} />
</Tooltip>
<Tooltip label='Event has delay' isDisabled={!hasDelay} {...tooltipProps}>
<span
className={`${style.statusIcon} ${style.statusDelay} ${
hasDelay ? style.enabled : ''
}`}
<Tooltip
label='Event has delay'
isDisabled={!hasDelay}
shouldWrapChildren {...tooltipProps}
>
<IoTimerOutline />
</span>
<IoTimerOutline className={`${style.statusIcon} ${style.statusDelay} ${
hasDelay ? style.enabled : ''
}`} />
</Tooltip>
<Tooltip
label={`${isPublic ? 'Event is public' : 'Event is private'}`}
{...tooltipProps}
shouldWrapChildren
>
<span
className={`${style.statusIcon} ${style.statusPublic} ${
<FiUsers className={`${style.statusIcon} ${style.statusPublic} ${
isPublic ? style.enabled : ''
}`}
>
<FiUsers />
</span>
}`} />
</Tooltip>
</div>
</div>
<div className={style.eventActions}>
<TooltipActionBtn
{...blockBtnStyle}
variant='ontime-subtle-white'
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' }}
backgroundColor={openId === eventId ? '#2B5ABC' : undefined}
color={openId === eventId ? 'white' : '#f6f6f6'}
/>
<EventBlockActionMenu
<BlockActionMenu
showAdd
showDelay
showBlock
showClone
enableDelete
actionHandler={actionHandler}
/>
</div>
@@ -0,0 +1,84 @@
import {
IconButton,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Tooltip,
} from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { IoTrashBinSharp } from '@react-icons/all-files/io5/IoTrashBinSharp';
import { tooltipDelayMid } from '../../../../ontimeConfig';
import { EventItemActions } from '../../RundownEntry';
interface BlockActionMenuProps {
showAdd?: boolean;
showDelay?: boolean;
showBlock?: boolean;
enableDelete?: boolean;
showClone?: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void;
className?: string;
}
export default function BlockActionMenu(props: BlockActionMenuProps) {
const { showAdd, showDelay, showBlock, enableDelete, showClone, actionHandler, className } = props;
return (
<Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<Tooltip label='Add ...' openDelay={tooltipDelayMid}>
<MenuButton
as={IconButton}
aria-label='Event options'
icon={<IoAdd />}
tabIndex={-1}
variant='ontime-subtle'
color='#f6f6f6'
size='sm'
className={className}
/>
</Tooltip>
<MenuList>
<MenuItem icon={<IoAdd />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
Add Event after
</MenuItem>
<MenuItem
icon={<IoTimerOutline />}
onClick={() => actionHandler('delay')}
isDisabled={!showDelay}
>
Add Delay after
</MenuItem>
<MenuItem
icon={<IoRemoveCircleOutline />}
onClick={() => actionHandler('block')}
isDisabled={!showBlock}
>
Add Block after
</MenuItem>
{showClone && (
<MenuItem
icon={<IoDuplicateOutline />}
onClick={() => actionHandler('clone')}
isDisabled={!showBlock}
>
Clone event
</MenuItem>
)}
<MenuDivider />
<MenuItem
icon={<IoTrashBinSharp />}
onClick={() => actionHandler('delete')}
isDisabled={!enableDelete}
color='#D20300'
>
Delete event
</MenuItem>
</MenuList>
</Menu>
);
}
@@ -1,87 +0,0 @@
import {
Divider,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Tooltip,
} from '@chakra-ui/react';
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 { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
import { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import PropTypes from 'prop-types';
import { tooltipDelayMid } from '../../../../ontimeConfig';
export default function EventBlockActionMenu(props) {
const { showAdd, showDelay, showBlock, showClone, actionHandler } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
const blockBtnStyle = {
size: 'sm',
variant: 'outline',
colorScheme: 'whiteAlpha',
};
return (
<Menu isLazy lazyBehavior='unmount'>
<Tooltip label='Add ...' delay={tooltipDelayMid}>
<MenuButton as={IconButton} aria-label='Options' icon={<FiPlus />}
tabIndex={-1} {...blockBtnStyle} />
</Tooltip>
<MenuList style={menuStyle}>
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
Add Event after
</MenuItem>
<MenuItem
icon={<IoTimerOutline />}
onClick={() => actionHandler('delay')}
isDisabled={!showDelay}
>
Add Delay after
</MenuItem>
<MenuItem
icon={<FiMinusCircle />}
onClick={() => actionHandler('block')}
isDisabled={!showBlock}
>
Add Block after
</MenuItem>
{showClone && (
<MenuItem
icon={<IoDuplicateOutline />}
onClick={() => actionHandler('clone')}
isDisabled={!showBlock}
>
Clone event
</MenuItem>
)}
<Divider />
<MenuItem
icon={<FiTrash2 />}
onClick={() => actionHandler('delete')}
isDisabled={!showBlock}
color='red.500'
>
Delete event
</MenuItem>
</MenuList>
</Menu>
);
}
EventBlockActionMenu.propTypes = {
showAdd: PropTypes.bool,
showDelay: PropTypes.bool,
showBlock: PropTypes.bool,
showClone: PropTypes.bool,
actionHandler: PropTypes.func,
};
@@ -1,5 +1,5 @@
import { useCallback, useContext } from 'react';
import TimeInput from 'common/components/input/TimeInput';
import TimeInput from 'common/components/input/time-input/TimeInput';
import { LoggingContext } from 'common/context/LoggingContext';
import { millisToMinutes } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time';
@@ -1,85 +1,45 @@
@use '../../../theme/main' as *;
.quickAdd {
background-color: $bg-container-l2;
display: flex;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
justify-content: center;
gap: 5%;
margin: 4px 2px 2px;
margin: 4px 2px;
font-size: 12px;
}
.createEvent,
.createDelay,
.createBlock {
.btnRow {
justify-self: center;
display: flex;
gap: 10%;
.quickBtn {
width: auto;
padding: 0 16px;
height: 24px;
text-align: center;
vertical-align: center;
font-weight: 600;
line-height: 21px;
border-radius: 2px;
opacity: 0.6;
cursor: pointer;
}
.createEvent {
border: 1px solid $light-bg;
color: lighten($light-bg, 30%);
&:hover {
background-color: $light-bg;
opacity: 1;
}
color: $light-bg;
}
.createDelay {
border: 1px solid $block-delay-color;
color: lighten($block-delay-color, 30%);
&:hover {
background-color: $block-delay-color;
opacity: 1;
}
color: $block-delay-color;
}
.createBlock {
border: 1px solid $block-block-color;
color: lighten($block-block-color, 30%);
&:hover {
background-color: $block-block-color;
opacity: 1;
}
color: $block-block-color;
}
}
.keyboard {
margin-left: 4px;
margin-left: 8px;
padding: 0 4px;
color: $label-gray;
border-radius: 2px;
background-color: rgba(0, 0, 0, 0.24);
background-color: rgba(0, 0, 0, 0.1);
}
.options {
display: flex;
flex-direction: column;
label {
opacity: 0.65;
transition: opacity 0.15s;
&:hover {
opacity: 1;
}
}
}
.disabled {
border-color: $text-gray-disabled;
color: $text-gray-disabled;
pointer-events: none;
cursor: not-allowed;
}
@@ -1,5 +1,5 @@
import { useCallback, useContext, useRef } from 'react';
import { Checkbox, Tooltip } from '@chakra-ui/react';
import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction';
@@ -31,7 +31,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const doStartTime = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null)
const doPublic = useRef<HTMLInputElement | null>(null);
const handleCreateEvent = useCallback((eventType: EventTypes) => {
switch (eventType) {
@@ -62,38 +62,45 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
return (
<div className={style.quickAdd}>
<Tooltip label='Add Event' openDelay={tooltipDelayMid}>
<span
className={style.createEvent}
onClick={() => handleCreateEvent('event')}
role='button'
>
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
</span>
</Tooltip>
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
<span
className={`${style.createDelay} ${disableAddDelay ? style.disabled : ''}`}
onClick={() => handleCreateEvent('delay')}
role='button'
>
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
</span>
</Tooltip>
<Tooltip label='Add Block' openDelay={tooltipDelayMid}>
<span
className={`${style.createBlock} ${disableAddBlock ? style.disabled : ''}`}
onClick={() => handleCreateEvent('block')}
role='button'
>
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
</span>
</Tooltip>
<div className={style.btnRow}>
<Tooltip label='Add Event' openDelay={tooltipDelayMid}>
<Button
onClick={() => handleCreateEvent('event')}
size='xs'
variant='ontime-subtle'
className={`${style.quickBtn} ${style.createBlock}`}
>
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
</Button>
</Tooltip>
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
<Button
onClick={() => handleCreateEvent('delay')}
size='xs'
variant='ontime-subtle'
disabled={disableAddDelay}
className={`${style.quickBtn} ${style.createDelay}`}
>
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
</Button>
</Tooltip>
<Tooltip label='Add Block' openDelay={tooltipDelayMid}>
<Button
onClick={() => handleCreateEvent('block')}
size='xs'
variant='ontime-subtle'
disabled={disableAddBlock}
className={`${style.quickBtn} ${style.createEvent}`}
>
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
</Button>
</Tooltip>
</div>
<div className={style.options}>
<Checkbox
ref={doStartTime}
size='sm'
colorScheme='blue'
variant='ontime-ondark'
defaultChecked={startTimeIsLastEnd}
>
Start time is last end
@@ -101,7 +108,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
<Checkbox
ref={doPublic}
size='sm'
colorScheme='blue'
variant='ontime-ondark'
defaultChecked={defaultPublic}
>
Event is public
@@ -82,7 +82,6 @@ export default function OntimeTable({ tableData, userFields, selectedId, handleU
);
const handleResetReordering = useCallback(() => {
console.log('called reset', defaultColumnOrder)
saveColumnOrder(defaultColumnOrder);
setColumnOrder(defaultColumnOrder);
}, [saveColumnOrder, setColumnOrder]);
+4 -66
View File
@@ -194,11 +194,11 @@
}
$bg-theme-light: #fcfcfc;
$cell-theme-light: #f6f6f6;
$cell-theme-light: #ececec;
$text-theme-light: #202020;
$bg-theme-dark: #121212;
$bg2-theme-dark: #1c1c1c;
$cell-theme-dark: #323232;
$cell-theme-dark: #2d2d2d;
$text-theme-dark: white;
.tableWrapper {
@@ -221,6 +221,7 @@ $text-theme-dark: white;
td {
background-color: $cell-theme-light;
border: 1px solid $bg-theme-light;
color: #121212;
}
.actionText:hover,
.actionIcon:hover,
@@ -242,6 +243,7 @@ $text-theme-dark: white;
td {
background-color: $cell-theme-dark;
border: 1px solid $bg-theme-dark;
color: #ececec;
}
.actionText:hover,
.actionIcon:hover,
@@ -306,67 +308,3 @@ svg {
transform: rotate(360deg);
}
}
@media print {
/* No margins on build ... */
* {
font-size: 10pt;
}
/* Ensure printable colours */
.tableWrapper,
.tableWrapper__dark {
background-color: white;
color: black;
* {
background-color: white;
}
td, th,
.selected > td {
background-color: #f6f6f6;
}
th {
width: auto;
}
.tableContainer {
overflow: visible;
}
/* Remove unnecessary sections */
.headerNow,
.headerRunning,
.headerActions,
.tableSettings,
.headerClock,
.actionIcon,
.delayCell,
.blockCell,
.noPrint {
display: none !important;
* {
display: none !important;
}
}
/* Overload styles */
.rowHeader {
font-weight: 600 !important;
}
.row {
height: fit-content;
}
}
/* ... set page margins */
@page {
size: auto;
margin: 0;
}
}
@@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { AutoTextArea } from 'common/components/input/AutoTextArea';
import { AutoTextArea } from 'common/components/input/auto-text-area/AutoTextArea';
import { TableSettingsContext } from 'common/context/TableSettingsContext';
import PropTypes from 'prop-types';
@@ -37,14 +37,13 @@ useEffect(() => {
return (
<AutoTextArea
size='sm'
borderColor='#0001'
value={value}
onChange={onChange}
onBlur={onBlur}
rows={3}
transition='none'
spellCheck={false}
color={theme === "dark" ? "#fffffa" : "black"}
isDark={theme === "dark"}
/>
);
}
@@ -7,21 +7,18 @@ import style from '../Table.module.scss';
export default function EventRow(props) {
const { row, index, selectedId, delay } = props;
const selected = row.original.id === selectedId;
const colours = getAccessibleColour(row.original.colour);
const colours = row.original.colour
? getAccessibleColour(row.original.colour)
: {};
return (
<tr {...row.getRowProps()} className={selected ? style.selected : ''} id={row.original.id}>
<td className={style.indexColumn}>{index}</td>
{row.cells.map((cell) => {
const { key, style, ...restCellProps } = cell.getCellProps();
const dynamicStyles =
selected &&
(cell.column.Header === 'Start' ||
cell.column.Header === 'End' ||
cell.column.Header === 'Duration' ||
cell.column.Header === 'Public')
? { ...style }
: { ...style, ...colours };
const dynamicStyles = { ...style, ...colours };
// Inject delay value if exits
if (delay !== 0 && delay != null) {
+2 -3
View File
@@ -1,4 +1,3 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
@@ -9,7 +8,7 @@ const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<StrictMode>
// <StrictMode>
<App />
</StrictMode>
// </StrictMode>
);
+8 -6
View File
@@ -1,4 +1,4 @@
@use './theme/main';
@use './theme/v2Styles' as *;
* {
-webkit-font-smoothing: antialiased;
@@ -10,6 +10,7 @@
body,
html,
.App {
font-family: $ontime-font-family;
margin: 0 auto;
overflow: hidden;
overflow: clip;
@@ -19,14 +20,15 @@ html,
-webkit-app-region: drag;
}
// workaround for chakra
// // https://github.com/chakra-ui/chakra-ui/issues/417
option {
color: black;
color: initial;
}
/* width */
::-webkit-scrollbar {
width: 0.5rem;
height: 0.5rem;
width: 6px;
}
/* Track */
@@ -37,11 +39,11 @@ option {
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
background: #2d2d2d;
border-radius: 2px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.23);
background: #404040;
}
+4 -9
View File
@@ -1,3 +1,5 @@
@use "./ontimeColours" as *;
$transition-time-action: 0.1s;
$transition-time-feedback: 0.3s;
@@ -111,15 +113,8 @@ textarea {
}
// Define style for a link
a {
&::after {
content: ' \2197';
color: $ontime-pink;
}
&:hover {
color: $ontime-pink;
}
a:hover {
color: $ontime-pink;
}
// horizontal separator
+18 -4
View File
@@ -1,10 +1,10 @@
@use "main" as *;
@use "v2Styles" as *;
//////////////////////////////////// general app elements
@mixin main-container {
background-color: $bg-gray-1000;
border: 1px solid $bg-gray-1000;
background-color: $bg-container-l1;
border: 1px solid $bg-container-l1;
border-radius: 4px;
}
@@ -14,7 +14,21 @@
}
@mixin third-container {
background-color: $bg-gray-1100;
background-color: $bg-container-l3;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 2px;
}
@mixin action-link {
color: $action-text-color;
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
transition-property: color;
transition-duration: $transition-time-action;
&:hover {
color: $ontime-color;
}
}
+108
View File
@@ -0,0 +1,108 @@
// from https://www.genomecolor.space/
$white-1: rgba(255, 255, 255, 0.01);
$white-3: rgba(255, 255, 255, 0.03);
$white-7: rgba(255, 255, 255, 0.07);
$white-9: rgba(255, 255, 255, 0.09);
$white-10: rgba(255, 255, 255, 0.10);
$white-13: rgba(255, 255, 255, 0.13);
$gray-50: #f6f6f6;
$gray-100: #ececec;
$gray-200: #e2e2e2;
$gray-300: #cfcfcf;
$gray-400: #b1b1b1;
$gray-500: #9d9d9d;
$gray-600: #8a8a8a;
$gray-700: #6c6c6c;
$gray-800: #595959;
$gray-900: #4c4c4c;
$gray-1000: #404040;
$gray-1050: #303030;
$gray-1100: #2d2d2d;
$gray-1200: #202020;
$gray-1300: #1a1a1a;
$gray-1350: #101010;
$pure-white: #fff;
$ui-white: $gray-50;
$ui-black: $gray-1350;
$orange-50: #FFFAF0;
$orange-100: #FFF4DF;
$orange-200: #FFEECE;
$orange-300: #FFE1AB;
$orange-400: #FFCC78;
$orange-500: #FFBC56;
$orange-600: #FFAB33;
$orange-700: #E69000;
$orange-800: #D27C00;
$orange-900: #B46D00;
$orange-1000: #975E00;
$orange-1100: #6A4400;
$orange-1200: #4C3200;
$orange-1300: #3D2900;
$orange-1350: #2E1F00;
$green-50: #EDFAEE;
$green-100: #DCF4DE;
$green-200: #CBEECF;
$green-300: #A9E0B0;
$green-400: #77C785;
$green-500: #55B469;
$green-600: #339E4E;
$green-700: #087A27;
$green-800: #006E1B;
$green-900: #006415;
$green-1000: #00570F;
$green-1100: #004108;
$green-1200: #003005;
$green-1300: #002703;
$green-1350: #001D02;
$blue-50: #F5F7FF;
$blue-100: #E3EAFF;
$blue-200: #D2DDFF;
$blue-300: #AFC4FF;
$blue-400: #779BE7;
$blue-500: #578AF4;
$blue-600: #3E75E8;
$blue-700: #2B5ABC;
$blue-800: #0A43B9;
$blue-900: #0036A6;
$blue-1000: #002A90;
$blue-1100: #001A64;
$blue-1200: #001145;
$blue-1300: #000D36;
$blue-1350: #000926;
$red-50: #FFF0F0;
$red-100: #FFDFDF;
$red-200: #FFCECE;
$red-300: #FFABAB;
$red-400: #FF7878;
$red-500: #FA5656;
$red-600: #ED3333;
$red-700: #D20300;
$red-800: #C10000;
$red-900: #B20000;
$red-1000: #9A0000;
$red-1100: #6F0000;
$red-1200: #520000;
$red-1300: #440000;
$red-1350: #360000;
$violet-50: #F9F7FE;
$violet-100: #F3EFFC;
$violet-200: #E7DFF6;
$violet-300: #CEBFEC;
$violet-400: #B8A0E3;
$violet-500: #AB8DB8;
$violet-600: #9771D3;
$violet-700: #8B60CA;
$violet-800: #8352C6;
$violet-900: #7945c1;
$violet-1000: #7248AD;
$violet-1100: #573486;
$violet-1200: #3C235F;
$violet-1300: #301A4D;
$violet-1350: #231339;
+47
View File
@@ -0,0 +1,47 @@
@use "./ontimeColours" as *;
$transition-time-action: 0.1s;
$transition-time-feedback: 0.3s;
$component-border-radius-md: 3px;
$component-border-radius-sm: 2px;
// semantic colours
$action-blue: #3182ce;
$action-text-color: $blue-400;
$ontime-color: #ff7597;
$error-red: $red-700;
$warning-orange: $orange-700;
$opacity-disabled: 0.4;
// playback colours
$playback-start: $green-600;
$ontime-roll: #0274B6;
$ontime-delay: #F57C13;
$ontime-delay-text: #E69056;
$ontime-paused: #c05621;
$ontime-stop: #E4281E;
$playback-negative: $red-500;
// interface panels
$bg-container-l1: $gray-1350;
$bg-container-l2: $gray-1100;
$bg-container-l3: $gray-1350;
$box-shadow-l1: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
$box-shadow-l2: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
// interface elements
$border-color-ondark: $white-10;
$element-inner-spacing: 4px;
$element-spacing: 8px;
$section-spacing: 16px;
$main-spacing: 32px;
// interface text
$ontime-font-family: "Open Sans", "Segoe UI", sans-serif;
$label-gray: $gray-400;
$secondary-text-gray: $gray-400;
$section-white: $ui-white;
$inner-section-text-size: 14px;
$text-body-size: 15px;
+3
View File
@@ -1,6 +1,9 @@
@use 'main' as *;
@use 'viewerCommon' as *;
// Text
$ontime-font-family: "Open Sans", "Segoe UI", sans-serif; // --font-family-override
// General styling
$accent-color: $ontime-pink; // --accent-color-override
$delay-color: $delay-text;
+53
View File
@@ -0,0 +1,53 @@
const commonStyles = {
letterSpacing: '0.3px',
fontWeight: '400',
borderRadius: '3px',
};
export const ontimeButtonFilled = {
...commonStyles,
background: '#2B5ABC', // $blue-700
color: '#fff', // pure-white
border: '1px solid #2B5ABC', // $blue-700
_hover: {
backgroundColor: '#0A43B9', // $blue-800
border: '1px solid #0A43B9', // $blue-800
},
_active: {
backgroundColor: '#0036A6', // blue-900
borderColor: '#002A90', // blue-1000
},
};
export const ontimeButtonOutlined = {
...commonStyles,
backgroundColor: '#2d2d2d', // $gray-1100
color: '#779BE7', // $blue-400
border: '1px solid #779BE7', // $blue-400
_hover: {
backgroundColor: '#404040', // $gray-1000
},
_active: {
backgroundColor: '#2d2d2d', // $gray-1100
borderColor: '#202020', // $gray-12000
},
};
export const ontimeButtonSubtle = {
...commonStyles,
backgroundColor: '#303030', // $gray-1050
color: '#779BE7', // $blue-400
border: '1px solid transparent',
_hover: {
background: '#404040', // $gray-1000
},
_active: {
backgroundColor: '#2d2d2d', // $gray-1100
borderColor: '#202020', // $gray-12000
},
};
export const ontimeButtonSubtleWhite = {
...ontimeButtonSubtle,
color: '#f6f6f6', // $gray-50
};
+21
View File
@@ -0,0 +1,21 @@
export const ontimeCheckboxOnDark = {
control: {
border: '1px',
borderColor: '#2d2d2d', // $gray-1100
backgroundColor: '#2d2d2d', // $gray-1100
_checked: {
borderColor: '#3182ce', // $action-blue
backgroundColor: '#3182ce', //$action-blue
},
_focus: {
boxShadow: '0 0 0 1px #578AF4'
}
},
label: {
fontWeight: '200',
color: '#9d9d9d', // $gray-500
_checked: {
color: '#779BE7', //$blue-400
}
},
};
+18
View File
@@ -0,0 +1,18 @@
export const ontimeMenuOnDark = {
list: {
borderRadius: "3px",
border: 'none',
bg: '#fff', // $gray-50
},
item: {
letterSpacing: '0.15px',
color: '#101010', // $gray-1350
bg: '#fff', //
_hover: {
backgroundColor: '#e2e2e2', // $gray-200
},
},
divider: {
borderColor: '#cfcfcf', // $gray-200
},
};
+20
View File
@@ -0,0 +1,20 @@
export const ontimeSelect = {
field: {
color: '#9d9d9d', // $gray-500
borderRadius: '3px',
fontWeight: '400',
background: '#2d2d2d', // $gray-1100
border: '1px solid transparent',
_hover: {
background: '#404040', // $gray-1000
},
_focus: {
background: '#404040', // $gray-1000
color: '#f6f6f6', // $gray-50
border: '1px solid #578AF4', // $blue-500
},
},
icon: {
color: '#9d9d9d', // $gray-500
},
};
+10
View File
@@ -0,0 +1,10 @@
export const ontimeSwitch = {
container: { },
track: {
background: '#2d2d2d', // $gray-1100
_checked: {
background: `#2B5ABC`, // $blue-700
},
},
thumb: {},
};
+43
View File
@@ -0,0 +1,43 @@
const commonStyles = {
borderRadius: '3px',
fontWeight: '400',
backgroundColor: '#2d2d2d', // $gray-1100
color: '#e2e2e2', // $gray-200
border: '1px solid transparent',
_hover: {
backgroundColor: '#404040', // $gray-1000
},
_focus: {
backgroundColor: '#404040', // $gray-1000
color: '#f6f6f6', // $gray-50
border: '1px solid #578AF4', // $blue-500
},
_placeholder: { color: '#9d9d9d' }, // $gray-500
};
export const ontimeInputFilled = {
field: {
...commonStyles,
},
};
export const ontimeTextAreaFilled = {
...commonStyles,
};
export const ontimeTextAreaFilledOnLight = {
borderRadius: '3px',
fontWeight: '400',
backgroundColor: '#ececec', // $gray-100
color: '#202020', // $gray-1200
border: '1px solid transparent',
_hover: {
backgroundColor: '#cfcfcf', // $gray-300
},
_focus: {
backgroundColor: '#cfcfcf', // $gray-300
color: '#101010',
border: '1px solid #578AF4', // $blue-500
},
_placeholder: { color: '#9d9d9d' }, // $gray-500
}
+12
View File
@@ -0,0 +1,12 @@
export const ontimeTooltip = {
_light: {
backgroundColor: '#2d2d2d',
color: '#779BE7',
padding: '2px 8px',
},
_dark: {
backgroundColor: '#2d2d2d',
color: '#779BE7',
padding: '2px 8px',
},
};
-50
View File
@@ -1,50 +0,0 @@
import { extendTheme } from '@chakra-ui/react';
const theme = extendTheme({
components: {
Button: {
baseStyle: {
borderRadius: '3px',
},
},
Input: {
baseStyle: {
field: {
borderRadius: 3,
},
},
sizes: {},
variants: {
filled: {
field: {
backgroundColor: 'rgba(255, 255, 255, 0.03)',
_hover: {
backgroundColor: 'rgba(255, 255, 255, 0.13)',
},
},
},
},
defaultProps: {
variant: null, // null here
},
},
Textarea: {
baseStyle: {
borderRadius: 3,
},
variants: {
filled: {
backgroundColor: 'rgba(255, 255, 255, 0.03)',
_hover: {
backgroundColor: 'rgba(255, 255, 255, 0.13)',
},
},
},
defaultProps: {
variant: null, // null here
},
},
},
});
export default theme;
+88
View File
@@ -0,0 +1,88 @@
import { extendTheme } from '@chakra-ui/react';
import {
ontimeButtonFilled,
ontimeButtonOutlined,
ontimeButtonSubtle,
ontimeButtonSubtleWhite,
} from './ontimeButton';
import { ontimeCheckboxOnDark } from './ontimeCheckbox';
import { ontimeMenuOnDark } from './ontimeMenu';
import { ontimeSelect } from './ontimeSelect';
import { ontimeSwitch } from './ontimeSwitch';
import {
ontimeInputFilled,
ontimeTextAreaFilled,
ontimeTextAreaFilledOnLight,
} from './ontimeTextInputs';
import { ontimeTooltip } from './ontimeTooltip';
const theme = extendTheme({
components: {
Button: {
baseStyle: {
borderRadius: '3px',
},
variants: {
'ontime-filled': { ...ontimeButtonFilled },
'ontime-outlined': { ...ontimeButtonOutlined },
'ontime-subtle': { ...ontimeButtonSubtle },
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
},
},
Checkbox: {
variants: {
'ontime-ondark': { ...ontimeCheckboxOnDark },
},
},
Editable: {
baseStyle: {
input: {
borderRadius: '2px',
width: '100%',
},
preview: {
width: '100%',
},
},
},
Input: {
baseStyle: {
borderRadius: '2px',
border: '1px',
},
variants: {
'ontime-filled': { ...ontimeInputFilled },
},
},
Textarea: {
baseStyle: {
borderRadius: '3px',
},
variants: {
'ontime-filled': { ...ontimeTextAreaFilled },
'ontime-filled-onlight': { ...ontimeTextAreaFilledOnLight },
},
},
Tooltip: {
baseStyle: { ...ontimeTooltip},
},
Switch: {
variants: {
'ontime': { ...ontimeSwitch },
},
},
Select: {
variants: {
'ontime': { ...ontimeSelect },
},
},
Menu: {
variants: {
'ontime-on-dark': { ...ontimeMenuOnDark },
},
},
},
});
export default theme;
+888 -978
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -116,7 +116,7 @@
},
"settings": {
"app": "ontime",
"version": 1,
"version": 2,
"serverPort": 4001,
"lock": null,
"pinCode": "1234"
+5 -1
View File
@@ -30,7 +30,11 @@ let isQuitting = false;
(async () => {
try {
const dbLoader = await import('./src/modules/loadDb.js');
const loadDepPath = isProduction
? path.join('file://', __dirname, '../', 'extraResources', 'src/modules/loadDb.js')
: path.join('file://', __dirname, 'src/modules/loadDb.js');
const dbLoader = await import(loadDepPath);
await dbLoader.promise;
const { startServer, startOSCServer } = await import(nodePath);

Some files were not shown because too many files have changed in this diff Show More