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", "version": "1.9.6",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/react": "2.3.4", "@chakra-ui/react": "^2.4.1",
"@dnd-kit/core": "^6.0.5", "@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1", "@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0", "@dnd-kit/utilities": "^3.2.0",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.4", "@emotion/styled": "^11.10.5",
"@react-icons/all-files": "^4.1.0", "@react-icons/all-files": "^4.1.0",
"@tanstack/react-query": "^4.10.3", "@tanstack/react-query": "^4.10.3",
"@tanstack/react-query-devtools": "^4.11.0", "@tanstack/react-query-devtools": "^4.11.0",
@@ -61,7 +61,7 @@
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.37.0", "@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^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": "^8.25.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest": "^27.0.4", "eslint-plugin-jest": "^27.0.4",
@@ -77,10 +77,10 @@
"stylelint-config-prettier": "^9.0.3", "stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard-scss": "^4.0.0", "stylelint-config-standard-scss": "^4.0.0",
"typescript": "^4.8.3", "typescript": "^4.8.3",
"vite": "^3.1.6", "vite": "^3.2.4",
"vite-plugin-svgr": "^2.2.1", "vite-plugin-svgr": "^2.2.2",
"vite-tsconfig-paths": "^3.5.0", "vite-tsconfig-paths": "^3.6.0",
"vitest": "^0.23.2" "vitest": "^0.25.3"
}, },
"resolutions": { "resolutions": {
"**/@types/react": "18.0.21" "**/@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; clickHandler: () => void;
size?: Size; 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) { export default function QuitIconBtn(props: QuitIconBtnProps) {
const { clickHandler, size = 'lg', ...rest } = props; const { clickHandler, size = 'lg', ...rest } = props;
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -47,11 +64,8 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
aria-label='Quit Application' aria-label='Quit Application'
size={size} size={size}
icon={<FiPower />} icon={<FiPower />}
colorScheme='red'
variant='outline'
isRound
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
_focus={{ boxShadow: 'none' }} {...quitBtnStyle}
{...rest} {...rest}
/> />
</Tooltip> </Tooltip>
@@ -59,7 +73,7 @@ export default function QuitIconBtn(props: QuitIconBtnProps) {
<AlertDialogOverlay> <AlertDialogOverlay>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'> <AlertDialogHeader fontSize='lg' fontWeight='bold'>
Server Shutdown Ontime Shutdown
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogBody> <AlertDialogBody>
This will shutdown the program and all running servers. Are you sure? 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 { .header {
padding: 0; font-size: $inner-section-text-size;
margin: 0; font-weight: 600;
font-size: 0.9em;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
color: $header-gray; color: $section-white;
} border-bottom: 1px solid $border-color-ondark;
padding-bottom: $element-inner-spacing;
margin-bottom: $element-spacing;
.moreExpanded,
.moreCollapsed {
cursor: pointer; cursor: pointer;
color: $text-white;
} }
.moreExpanded { .moreExpanded {
transform: scaleY(-1); transform: scaleY(-1);
transition: transform 0.3s; transition: transform $transition-time-feedback;
} }
.moreCollapsed { .moreCollapsed {
transform: scaleY(1); 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 *; @use '../../../theme/viewerDefs' as *;
.timer { .timer {
font-family: 'Open Sans', sans-serif; font-family: var(--font-family-override, $ontime-font-family);
color: var(--timer-color-override, $viewer-color); color: var(--timer-color-override, $viewer-color);
font-size: 21vw; font-size: 21vw;
line-height: 21vw; line-height: 21vw;
@@ -9,7 +9,7 @@
letter-spacing: 1vw; letter-spacing: 1vw;
&--small { &--small {
font-size: 4em; font-size: 3.75em;
line-height: 0.9; line-height: 0.9;
text-align: center; text-align: center;
letter-spacing: 0.1em; 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"] { input[type="color"] {
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
@@ -12,7 +12,7 @@ export default function ColourInput(props: ColourInputProps) {
return ( return (
<Input <Input
size='sm' size='sm'
variant='filled' variant='ontime-filled'
className={style.colourInput} className={style.colourInput}
type='color' type='color'
value={value} 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 { clamp } from 'common/utils/math';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import style from './TimeInput.module.scss'; import style from './DelayInput.module.scss';
const inputProps = { const inputStyleProps = {
width: 20, width: 20,
backgroundColor: 'rgba(255,255,255,0.13)',
color: '#fff',
border: '1px solid #ecc94b55',
variant: 'filled',
borderRadius: '3px',
placeholder: '-', placeholder: '-',
textAlign: 'center',
size: 'sm', size: 'sm',
color: '#E69056',
variant: 'ontime-filled',
}; };
export default function DelayInput(props) { interface DelayInputProps {
const { submitHandler, value } = props; submitHandler: (value: number) => void;
value?: number;
}
export default function DelayInput(props: DelayInputProps) {
const { submitHandler, value = 0 } = props;
const [_value, setValue] = useState(value); const [_value, setValue] = useState(value);
const inputRef = useRef(null); const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => { useEffect(() => {
if (value == null) return; if (value == null) return;
@@ -32,7 +33,8 @@ export default function DelayInput(props) {
* @param {string} value string to be parsed * @param {string} value string to be parsed
*/ */
const validate = useCallback( const validate = useCallback(
(newValue) => { (newValue?: string) => {
console.log('debug', newValue, typeof newValue);
if (newValue === '') setValue(0); if (newValue === '') setValue(0);
const delayValue = clamp(Number(newValue), -60, 60); const delayValue = clamp(Number(newValue), -60, 60);
@@ -41,42 +43,42 @@ export default function DelayInput(props) {
submitHandler(delayValue); submitHandler(delayValue);
}, },
[submitHandler, value] [submitHandler, value],
); );
/** /**
* @description Handles common keys for submit and cancel * @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event * @param {KeyboardEvent} event
*/ */
const onKeyDownHandler = useCallback((event) => { const onKeyDownHandler = useCallback((key: string) => {
if (event.key === 'Enter') { if (key === 'Enter') {
inputRef.current.blur(); inputRef.current?.blur();
validate(event.target.value); validate(inputRef.current?.value);
} else if (event.key === 'Escape') { } else if (key === 'Escape') {
inputRef.current.blur(); inputRef.current?.blur();
setValue(value); setValue(value);
} }
}, [validate, value]); }, [validate, value]);
const labelText = `${Math.abs(value) > 1 ? 'minutes' : 'minute'} ${ const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
value >= 0 ? 'delayed' : 'ahead' value !== undefined && value >= 0 ? 'delayed' : 'ahead'
}`; }`;
return ( return (
<div className={style.delayInput}> <label className={style.delayInput}>
<Input <Input
{...inputStyleProps}
ref={inputRef} ref={inputRef}
data-testid='delay-input' data-testid='delay-input'
className={style.inputField} className={style.inputField}
{...inputProps}
value={_value} value={_value}
onChange={(event) => setValue(event.target.value)} onChange={(event) => setValue(Number(event.target.value))}
onBlur={() => setValue(value)} onBlur={(event) => validate(event.target.value)}
onKeyDown={onKeyDownHandler} onKeyDown={(event) => onKeyDownHandler(event.key)}
type='number' type='number'
/> />
<span className={style.label}>{labelText}</span> {labelText}
</div> </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', () => { it('renders correctly', () => {
const testField = 'test'; const testField = 'test';
const testText = 'Test 123'; 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'); const input = screen.getByTestId('input-textfield');
expect(input).toBeInTheDocument(); expect(input).toBeInTheDocument();
@@ -19,7 +20,9 @@ describe('TextInput component', () => {
it('Handles renders as textarea', () => { it('Handles renders as textarea', () => {
const testField = 'test'; const testField = 'test';
const testText = 'Test 123'; 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'); const input = screen.getByTestId('input-textarea');
expect(input).toBeInTheDocument(); expect(input).toBeInTheDocument();
@@ -87,7 +90,7 @@ describe('TextInput component', () => {
it('handles undefined value', () => { it('handles undefined value', () => {
const testField = 'test'; const testField = 'test';
const expected = ''; const expected = '';
render(<TextInput field={testField} />); render(<TextInput field={testField} submitHandler={vi.fn()} />);
const input = screen.getByTestId('input-textfield'); const input = screen.getByTestId('input-textfield');
expect(input).toBeInTheDocument(); expect(input).toBeInTheDocument();
expect(input).toHaveValue(expected); 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; @use "../../../theme/v2Styles" as *;
$border-color: rgba(white, 0.1); @use "../../../theme/mixins" as *;
$menu-bg: #202020; @use "../../../theme/ontimeColours" as *;
$menu-hover-bg: #101010;
$menu-focus-bg: #181818;
$menu-box-shadow: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
$icon-color: #f6f6f6; $menu-bg: $gray-1200;
$button-bg: #303030; $menu-hover-bg: $gray-1350;
$menu-focus-bg: $gray-1300;
$icon-color: $ui-white;
$button-bg: $gray-1050;
$button-size: 48px; $button-size: 48px;
$ontime-pink: #ff7597;
// Todo: remove important once style is retired
a {
&::after {
content: none !important;
}
}
.mirror { .mirror {
transform: rotate(180deg); transform: rotate(180deg);
} }
@@ -47,15 +39,14 @@ a {
.menuContainer { .menuContainer {
top: 0; top: 0;
left: 0; left: 0;
font-family: 'Open Sans', sans-serif;
height: fit-content; height: fit-content;
position: absolute; position: absolute;
background-color: $menu-bg; background-color: $menu-bg;
min-width: 200px; min-width: 200px;
border-radius: 0 0 24px 0; 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; padding-bottom: 1rem;
max-height: 100vh; max-height: 100vh;
@@ -67,21 +58,17 @@ a {
} }
.link { .link {
color: $action-text-color; @include action-link;
display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
cursor: pointer; cursor: pointer;
font-size: 14px;
&:hover { &:hover {
background-color: $menu-hover-bg; background-color: $menu-hover-bg;
color: $ontime-pink;
} }
&:active { &:active {
background-color: $border-color; background-color: $border-color-ondark;
} }
&:focus { &:focus {
@@ -96,12 +83,11 @@ a {
} }
} }
.linkIcon { .linkIcon {
display: inline-block; display: inline-block;
transform: rotate(45deg); transform: rotate(45deg);
} }
.separator { .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 { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
import { generateId } from '../utils/generate_id'; import { generateId } from '../utils/generate_id';
import socket from '../utils/socket';
import { nowInMillis, stringFromMillis } from '../utils/time'; import { nowInMillis, stringFromMillis } from '../utils/time';
import socket from '../utils/socket'; export enum LOG_LEVEL {
type LOG_LEVEL = 'INFO' | 'WARN' | 'ERROR'; INFO = "INFO",
type Log = { WARN = "WARN",
ERROR = "ERROR",
}
export type Log = {
id: string; id: string;
origin: string; origin: string;
time: string; time: string;
@@ -90,7 +95,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
*/ */
const emitInfo = useCallback( const emitInfo = useCallback(
(text: string) => { (text: string) => {
_send(text, 'INFO'); _send(text, LOG_LEVEL.INFO);
}, },
[_send], [_send],
); );
@@ -101,7 +106,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
*/ */
const emitWarning = useCallback( const emitWarning = useCallback(
(text: string) => { (text: string) => {
_send(text, 'WARN'); _send(text, LOG_LEVEL.WARN);
}, },
[_send], [_send],
); );
@@ -112,7 +117,7 @@ export const LoggingProvider = ({ children }: LoggingProviderProps) => {
*/ */
const emitError = useCallback( const emitError = useCallback(
(text: string) => { (text: string) => {
_send(text, 'ERROR'); _send(text, LOG_LEVEL.ERROR);
}, },
[_send], [_send],
); );
+2 -2
View File
@@ -46,7 +46,7 @@ const emptyMessageControl = {
}; };
export const useMessageControl = createSocketHook(FEAT_MESSAGECONTROL, emptyMessageControl); export const useMessageControl = createSocketHook(FEAT_MESSAGECONTROL, emptyMessageControl);
export const setMessage = () => ({ export const setMessage = {
presenterText: (payload: string) => socket.emit('set-timer-message-text', payload), presenterText: (payload: string) => socket.emit('set-timer-message-text', payload),
presenterVisible: (payload: boolean) => socket.emit('set-timer-message-visible', payload), presenterVisible: (payload: boolean) => socket.emit('set-timer-message-visible', payload),
publicText: (payload: string) => socket.emit('set-public-message-text', 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), lowerText: (payload: string) => socket.emit('set-lower-message-text', payload),
lowerVisible: (payload: boolean) => socket.emit('set-lower-message-visible', payload), lowerVisible: (payload: boolean) => socket.emit('set-lower-message-visible', payload),
onAir: (payload: boolean) => socket.emit('set-onAir', payload), onAir: (payload: boolean) => socket.emit('set-onAir', payload),
}); };
export const emptyPlaybackControl = { export const emptyPlaybackControl = {
playback: 'stop', playback: 'stop',
+7 -2
View File
@@ -1,8 +1,13 @@
import { OntimeSettingsType } from './OntimeSettings.type'; import { OntimeSettingsType } from './OntimeSettings.type';
type NetworkInterfaceType = {
name: string;
address: string;
}
export type InfoType = { export type InfoType = {
networkInterfaces: string[]; networkInterfaces: NetworkInterfaceType[];
settings: Pick<OntimeSettingsType, "version" | "serverPort" > settings: Pick<OntimeSettingsType, 'version' | 'serverPort'>
} }
export const ontimePlaceholderInfo: InfoType = { 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 { 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 TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { tooltipDelayMid } from '../../../ontimeConfig'; import { tooltipDelayMid } from '../../../ontimeConfig';
import style from './MessageControl.module.scss'; import style from './InputRow.module.scss';
interface InputRowProps { interface InputRowProps {
label: string; label: string;
placeholder: string; placeholder: string;
text: string; text: string;
visible: boolean; visible?: boolean;
actionHandler: (action: string, payload: object) => void; actionHandler: (action: string, payload: object) => void;
changeHandler: (newValue: string) => void; changeHandler: (newValue: string) => void;
} }
@@ -23,12 +24,12 @@ export default function InputRow(props: InputRowProps) {
}; };
return ( return (
<div className={`${visible ? style.inputRowActive : ''}`}> <div className={style.inputRow}>
<label className={style.label}>{label}</label> <label className={`${style.label} ${visible ? style.active : ''}`}>{label}</label>
<div className={style.inputItems}> <div className={style.inputItems}>
<Input <Input
size='sm' size='sm'
variant='filled' variant='ontime-filled'
value={text} value={text}
onChange={(event) => handleInputChange(event.target.value)} onChange={(event) => handleInputChange(event.target.value)}
placeholder={placeholder} placeholder={placeholder}
@@ -38,9 +39,8 @@ export default function InputRow(props: InputRowProps) {
tooltip={visible ? 'Make invisible' : 'Make visible'} tooltip={visible ? 'Make invisible' : 'Make visible'}
aria-label={`Toggle ${label}`} aria-label={`Toggle ${label}`}
openDelay={tooltipDelayMid} openDelay={tooltipDelayMid}
icon={<IoSunny size='18px' />} icon={visible? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
colorScheme='blue' variant={visible ? 'ontime-filled' : 'ontime-subtle'}
variant={visible ? 'solid' : 'outline'}
size='sm' size='sm'
/> />
</div> </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/v2Styles' as *;
@use '../../../theme/mixins' as *;
@mixin message-control-label() {
font-size: 0.9em;
color: $label-gray;
}
.messageContainer,
.onAirToggle {
display: flex;
padding: 0.5em;
}
.messageContainer { .messageContainer {
display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: $section-spacing;
.inputItems {
display: grid;
grid-template-columns: 1fr auto;
gap: 1em;
}
.label {
padding: 0;
margin: 0;
@include message-control-label;
}
.inputRowActive {
.label {
color: $action-blue;
}
}
} }
.onAirToggle { .onAirSection {
margin-top: 1em; margin-top: $section-spacing;
display: grid; display: flex;
grid-template-areas: flex-direction: column;
'btn label' gap: $element-spacing;
'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;
}
} }
.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 { 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 ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
import { handleLinks } from '../../../common/utils/linkUtils'; import { handleLinks } from '../../../common/utils/linkUtils';
@@ -11,7 +11,7 @@ import style from '../../editors/Editor.module.scss';
export default function MessageControlExport() { export default function MessageControlExport() {
return ( return (
<Box className={style.messages} data-testid="panel-messages-control"> <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}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
<MessageControl /> <MessageControl />
@@ -1,11 +1,9 @@
import { Tooltip } from '@chakra-ui/react';
import { IoPause } from '@react-icons/all-files/io5/IoPause'; import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline'; import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { setPlayback } from '../../../common/hooks/useSocket'; import { setPlayback } from '../../../common/hooks/useSocket';
import { Playstate } from '../../../common/models/OntimeTypes'; import { Playstate } from '../../../common/models/OntimeTypes';
import { tooltipDelayMid } from '../../../ontimeConfig';
import TapButton from './TapButton'; import TapButton from './TapButton';
@@ -13,48 +11,44 @@ import style from './PlaybackControl.module.scss';
interface PlaybackProps { interface PlaybackProps {
playback: Playstate; playback: Playstate;
selectedId: string; selectedId: string | null;
noEvents: boolean; noEvents: boolean;
} }
export default function Playback(props: PlaybackProps) { export default function Playback(props: PlaybackProps) {
const { playback, selectedId, noEvents } = props; const { playback, selectedId, noEvents } = props;
const isRolling = playback === 'roll'; const isRolling = playback === 'roll';
const isPlaying = playback === 'start';
const isPaused = playback === 'pause';
return ( return (
<div className={style.playbackContainer}> <div className={style.playbackContainer}>
<Tooltip label='Start playback' openDelay={100}> <TapButton
<TapButton onClick={() => setPlayback.start()}
onClick={() => setPlayback.start()} disabled={!selectedId || isRolling || noEvents}
disabled={!selectedId || isRolling || noEvents} theme='start'
theme='start' active={isPlaying}
active={playback === 'start'} >
> <IoPlay />
<IoPlay /> </TapButton>
</TapButton>
</Tooltip>
<Tooltip label='Pause playback' openDelay={tooltipDelayMid}> <TapButton
<TapButton onClick={() => setPlayback.pause()}
onClick={() => setPlayback.pause()} disabled={!selectedId || isRolling || noEvents}
disabled={!selectedId || isRolling || noEvents || playback !== 'start'} theme='pause'
theme='pause' active={isPaused}
active={playback === 'pause'} >
> <IoPause />
<IoPause /> </TapButton>
</TapButton>
</Tooltip>
<Tooltip label='Start roll mode' openDelay={tooltipDelayMid}> <TapButton
<TapButton onClick={() => setPlayback.roll()}
onClick={() => setPlayback.roll()} disabled={noEvents}
disabled={playback === 'roll' || noEvents} theme='roll'
theme='roll' active={isRolling}
active={isRolling} >
> <IoTimeOutline />
<IoTimeOutline /> </TapButton>
</TapButton>
</Tooltip>
</div> </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 *; @use '../../../theme/mixins' as *;
.mainContainer { .mainContainer {
width: 100%; width: 100%;
display: grid; display: grid;
margin: 0 auto; margin: 0 auto;
gap: 4px; gap: $element-inner-spacing;
} }
.timeContainer { .timeContainer {
display: grid; display: grid;
grid-template-areas: grid-template-areas:
@@ -16,7 +16,7 @@
'... sta fin btn'; '... sta fin btn';
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
grid-template-columns: 1.5em 1fr 1fr 5em; grid-template-columns: 1.5em 1fr 1fr 5em;
gap: 4px; gap: $element-inner-spacing;
justify-items: start; justify-items: start;
} }
@@ -38,16 +38,16 @@
.indRoll, .indRoll,
.indDelay, .indDelay,
.indNegative { .indNegative {
background-color: $bg-black-300; background-color: $gray-1300;
} }
.indRoll, .indRoll,
.indRollActive, .indRollActive,
.indDelay { .indDelay {
margin: 0 auto; margin: 0 auto;
border-radius: 50%; border-radius: 6px;
width: 0.8em; width: 12px;
height: 0.8em; height: 12px;
} }
.indRollActive { .indRollActive {
@@ -58,11 +58,11 @@
.indNegativeActive { .indNegativeActive {
margin: 0 auto; margin: 0 auto;
width: 90%; width: 90%;
height: 0.3em; height: 4px;
} }
.indNegativeActive { .indNegativeActive {
background-color: $ontime-pink-variant; background-color: $playback-negative;
} }
.indDelayActive { .indDelayActive {
@@ -75,7 +75,7 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr; grid-template-rows: 1fr 1fr;
width: 100%; width: 100%;
gap: 4px; gap: $element-inner-spacing;
} }
.minus { .minus {
@@ -103,23 +103,27 @@
} }
.time { .time {
color: $header-gray; color: $section-white;
font-size: 1.1em; font-size: $text-body-size;
} }
.tag { .tag {
color: $label-gray; color: $label-gray;
font-size: 0.9em; font-size: 13px;
} }
.rolltag { .rolltag {
color: $ontime-roll; color: $ontime-roll;
font-size: 0.9em; font-size: $text-body-size;
} }
.playbackContainer { .playbackContainer {
display: flex; display: flex;
justify-content: space-evenly; justify-content: space-evenly;
padding-top: 0.5em; 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}> <div className={style.btn}>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} <Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}> shouldWrapChildren={disableButtons}>
<TapButton <TapButton
onClick={() => setPlayback.delay(-5)} onClick={() => setPlayback.delay(-5)}
@@ -68,33 +68,33 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
-5 -5
</TapButton> </TapButton>
</Tooltip> </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} <Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}> shouldWrapChildren={disableButtons}>
<TapButton <TapButton
onClick={() => setPlayback.delay(-5)} onClick={() => setPlayback.delay(-1)}
disabled={disableButtons} disabled={disableButtons}
square> square>
-1 -1
</TapButton> </TapButton>
</Tooltip> </Tooltip>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} <Tooltip label='Add 1 minute' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}> shouldWrapChildren={disableButtons}>
<TapButton <TapButton
onClick={() => setPlayback.delay(-5)} onClick={() => setPlayback.delay(1)}
disabled={disableButtons} disabled={disableButtons}
square> square>
1 1
</TapButton> </TapButton>
</Tooltip> </Tooltip>
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid}
shouldWrapChildren={disableButtons}>
<TapButton
onClick={() => setPlayback.delay(-5)}
disabled={disableButtons}
square>
5
</TapButton>
</Tooltip>
</div> </div>
</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) { $button-bg-gray: $gray-1050;
font-family: "Open Sans", sans-serif; $button-color-white: $gray-50;
@mixin tap-factory($theme-color) {
font-family: $ontime-font-family;
font-size: 22px; font-size: 22px;
background-color: $theme-bg; border-radius: $component-border-radius-md;
color: $theme-front;
border-radius: 3px;
width: 100%; width: 100%;
aspect-ratio: 3/1; aspect-ratio: 3/1;
box-shadow: $theme-high 0 0 2px; transition-property: color, background-color;
transition: background-color 0.3s; transition-duration: $transition-time-feedback;
display: grid; display: grid;
place-content: center; place-content: center;
letter-spacing: 0.3px;
background-color: $button-bg-gray;
color: $theme-color;
&:disabled { &:disabled {
cursor: not-allowed; cursor: not-allowed;
background-color: $opacity-disabled;
box-shadow: none;
opacity: $opacity-disabled; opacity: $opacity-disabled;
} }
&:hover:not(:disabled) { &:hover:not(:disabled) {
background-color: $theme-high; color: $button-color-white;
background-color: $theme-color;
} }
&:active:not(:disabled) { &:active:not(:disabled) {
color: $theme-bg; color: $button-bg-gray;
background-color: $theme-front; background-color: $button-color-white;
transition: background-color 0.15s; transition-property: color, background-color;
transition-duration: $transition-time-action;
} }
&.active { &.active {
background-color: $theme-high; background-color: $theme-color;
color: $button-color-white;
} }
} }
.tapButton.neutral{ .tapButton.neutral {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, #363636); @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 { .tapButton.start {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-accent); @include tap-factory($playback-start);
} }
.tapButton.roll { .tapButton.roll {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-roll); @include tap-factory($ontime-roll);
} }
.tapButton.pause { .tapButton.pause {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-paused); @include tap-factory($ontime-paused);
} }
.tapButton.ontime { .tapButton.ontime {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-pink); @include tap-factory($ontime-color);
} }
.tapButton.stop { .tapButton.stop {
@include tap-factory(rgba(255, 255, 255, 0.867), #303030, $ontime-red); @include tap-factory($ontime-stop);
} }
.tapButton.square { .tapButton.square {
@@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react'; 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 ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
import { handleLinks } from '../../../common/utils/linkUtils'; import { handleLinks } from '../../../common/utils/linkUtils';
@@ -10,8 +10,8 @@ import style from '../../editors/Editor.module.scss';
export default function TimerControlExport() { export default function TimerControlExport() {
return ( return (
<Box className={style.playback} data-testid="panel-timer-control"> <Box className={style.playback} data-testid='panel-timer-control'>
<FiArrowUpRight className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} /> <IoArrowUp className={style.corner} onClick={(event) => handleLinks(event, 'timercontrol')} />
<div className={style.content}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
<PlaybackControl /> <PlaybackControl />
@@ -1,7 +1,7 @@
import { Tooltip } from '@chakra-ui/react'; 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 { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward'; 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 { IoStop } from '@react-icons/all-files/io5/IoStop';
import { setPlayback } from '../../../common/hooks/useSocket'; import { setPlayback } from '../../../common/hooks/useSocket';
@@ -14,7 +14,7 @@ import style from './PlaybackControl.module.scss';
interface TransportProps { interface TransportProps {
playback: Playstate; playback: Playstate;
selectedId: string; selectedId: string | null;
noEvents: boolean; noEvents: boolean;
} }
@@ -45,10 +45,10 @@ export default function Transport(props: TransportProps) {
onClick={() => setPlayback.reload()} onClick={() => setPlayback.reload()}
disabled={selectedId == null || isRolling || noEvents} disabled={selectedId == null || isRolling || noEvents}
> >
<IoPlayBack /> <IoReload className={style.invertX} />
</TapButton> </TapButton>
</Tooltip> </Tooltip>
<Tooltip label='Unload Event' openDelay={100}> <Tooltip label='Unload Event' openDelay={tooltipDelayMid}>
<TapButton <TapButton
onClick={() => setPlayback.stop()} onClick={() => setPlayback.stop()}
disabled={(selectedId == null && !isRolling) || noEvents} disabled={(selectedId == null && !isRolling) || noEvents}
+11 -9
View File
@@ -5,12 +5,13 @@
top: $distance; top: $distance;
right: $distance; right: $distance;
cursor: pointer; cursor: pointer;
color: $ontime-pink; color: #f2f2f2;
} }
.corner { .corner {
display: none; display: none;
@include absolute-top-right(8px); @include absolute-top-right(8px);
transform: rotate(45deg);
} }
.mainContainer { .mainContainer {
@@ -20,10 +21,11 @@
margin: auto; margin: auto;
color: $title-white; color: $title-white;
padding: max(16px, 2vh) max(8px, 1vh); padding: max(16px, 2vh) max(8px, 1vh);
font-family: "Open Sans", "Segoe UI", sans-serif;
display: grid; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
grid-template-columns: 48px 46em 30em auto; grid-template-columns: 48px 46em 450px auto;
grid-template-areas: grid-template-areas:
'sett even play info' 'sett even play info'
'sett even mess info'; 'sett even mess info';
@@ -130,17 +132,17 @@
} }
.eventEditor { .eventEditor {
border-radius: 3px 3px 0 0; border-radius: 8px 8px 0 0;
background-color: $bg-black; background-color: #202020;
box-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px;
border-top: 1px solid $bg-gray-900; border-top: 1px solid $bg-gray-900;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
width: 100vw; width: 100vw;
max-height: 40vh;
left: 0; left: 0;
z-index: 10; z-index: 10;
color: white; color: white;
transition: bottom 0.3s; transition: bottom 0.3s; // $transition-time
&.noEvent { &.noEvent {
bottom: -500px; bottom: -500px;
@@ -152,7 +154,7 @@
} }
.header { .header {
background-color: $bg-black; background-color: #202020;
padding: 8px; padding: 8px;
border-left: 1px solid $bg-container-l3; border-left: 1px solid $bg-container-l3;
} }
@@ -186,9 +188,9 @@
.playback { .playback {
grid-area: play; grid-area: play;
min-height: 275px; min-height: 250px;
max-height: 380px; max-height: 380px;
min-width: 480px; min-width: 450px;
} }
.mainContainer > .settings { .mainContainer > .settings {
@@ -1,11 +1,10 @@
import { useCallback, useContext, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useState } from 'react';
import { Button, Select } from '@chakra-ui/react'; import { Button, Select, Switch } from '@chakra-ui/react';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { IoBan } from '@react-icons/all-files/io5/IoBan'; import { IoBan } from '@react-icons/all-files/io5/IoBan';
import { editorEventId } from 'common/atoms/LocalEventSettings'; import { editorEventId } from 'common/atoms/LocalEventSettings';
import ColourInput from 'common/components/input/ColourInput'; import ColourInput from 'common/components/input/colour-input/ColourInput';
import TextInput from 'common/components/input/TextInput'; import TextInput from 'common/components/input/text-input/TextInput';
import TimeInput from 'common/components/input/TimeInput'; import TimeInput from 'common/components/input/time-input/TimeInput';
import { LoggingContext } from 'common/context/LoggingContext'; import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction'; import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig'; import { millisToMinutes } from 'common/utils/dateConfig';
@@ -14,10 +13,10 @@ import { stringFromMillis } from 'common/utils/time';
import { calculateDuration, validateEntry } from 'common/utils/timesManager'; import { calculateDuration, validateEntry } from 'common/utils/timesManager';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import CopyTag from '../../common/components/copy-tag/CopyTag';
import useRundown from '../../common/hooks-query/useRundown'; import useRundown from '../../common/hooks-query/useRundown';
import style from './EventEditor.module.scss'; import style from './EventEditor.module.scss';
import CopyTag from '../../common/components/osc-tag/CopyTag';
export default function EventEditor() { export default function EventEditor() {
const [openId] = useAtom(editorEventId); const [openId] = useAtom(editorEventId);
@@ -93,7 +92,7 @@ export default function EventEditor() {
); );
if (!event) { if (!event) {
return <span>Loading</span>; return <span>Loading...</span>;
} }
const delayed = delay !== 0; const delayed = delay !== 0;
@@ -105,6 +104,10 @@ export default function EventEditor() {
return ( return (
<div className={style.eventEditor}> <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.timeOptions}>
<div className={style.timers}> <div className={style.timers}>
<label className={style.inputLabel}> <label className={style.inputLabel}>
@@ -116,7 +119,7 @@ export default function EventEditor() {
submitHandler={handleSubmit} submitHandler={handleSubmit}
validationHandler={timerValidationHandler} validationHandler={timerValidationHandler}
time={event.timeStart} time={event.timeStart}
delay={0} delay={delay}
placeholder='Start' placeholder='Start'
/> />
<label className={style.inputLabel}> <label className={style.inputLabel}>
@@ -128,7 +131,7 @@ export default function EventEditor() {
submitHandler={handleSubmit} submitHandler={handleSubmit}
validationHandler={timerValidationHandler} validationHandler={timerValidationHandler}
time={event.timeEnd} time={event.timeEnd}
delay={0} delay={delay}
placeholder='End' placeholder='End'
/> />
<label className={style.inputLabel}>Duration</label> <label className={style.inputLabel}>Duration</label>
@@ -137,24 +140,33 @@ export default function EventEditor() {
submitHandler={handleSubmit} submitHandler={handleSubmit}
validationHandler={timerValidationHandler} validationHandler={timerValidationHandler}
time={event.duration} time={event.duration}
delay={0} delay={delay}
placeholder='Duration' placeholder='Duration'
/> />
</div> </div>
<div className={style.timeSettings}> <div className={style.timeSettings}>
<label className={style.inputLabel}>Timer type</label> <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='option1'>Start to end</option>
<option value='option2'>Duration</option> <option value='option2'>Duration</option>
<option value='option3'>Follow previous</option> <option value='option3'>Follow previous</option>
<option value='option3'>Start only</option> <option value='option3'>Start only</option>
</Select> </Select>
<label className={style.inputLabel}>Countdown style</label> <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='option1'>Count down</option>
<option value='option2'>Count up</option> <option value='option2'>Count up</option>
<option value='option3'>Clock</option> <option value='option3'>Clock</option>
</Select> </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> </div>
<div className={style.titles}> <div className={style.titles}>
@@ -175,17 +187,6 @@ export default function EventEditor() {
<label className={style.inputLabel}>Subtitle</label> <label className={style.inputLabel}>Subtitle</label>
<TextInput field='subtitle' initialText={event.subtitle} submitHandler={handleSubmit} /> <TextInput field='subtitle' initialText={event.subtitle} submitHandler={handleSubmit} />
</div> </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>
<div className={style.right}> <div className={style.right}>
<div className={style.column}> <div className={style.column}>
@@ -196,28 +197,26 @@ export default function EventEditor() {
handleChange={(value) => handleSubmit('colour', value)} handleChange={(value) => handleSubmit('colour', value)}
/> />
<Button <Button
leftIcon={<IoBan />} rightIcon={<IoBan />}
onClick={() => handleSubmit('colour', '')} onClick={() => handleSubmit('colour', '')}
variant='ghost' variant='ontime-subtle'
colorScheme='blue'
borderRadius='3px'
size='sm' size='sm'
> >
Clear colour Clear colour
</Button> </Button>
</div> </div>
</div> </div>
<div className={style.column}> <div className={`${style.column} ${style.fullHeight}`}>
<label className={style.inputLabel}>Note</label> <label className={style.inputLabel}>Note</label>
<TextInput <TextInput
field='note' field='note'
initialText={event.note} initialText={event.note}
submitHandler={handleSubmit} submitHandler={handleSubmit}
isTextArea isTextArea
isFullHeight
/> />
</div> </div>
</div> </div>
<CopyTag label='OSC trigger' className={style.osc}>{`/ontime/gotoid/${event.id}`}</CopyTag>
</div> </div>
</div> </div>
); );
@@ -1,102 +1,104 @@
@use '../../theme/main' as *; @use '../../theme/v2Styles' as *;
.eventEditor { .eventEditor {
padding: 16px 32px; padding: 16px 32px 32px 32px;
width: 100%; width: 100%;
overflow-y: scroll;
max-height: 40vh;
gap: max(16px, 2vh); gap: max(16px, 2vh);
display: grid; display: grid;
grid-template-areas: 'timeOptions titles'; grid-template-areas:
'eventInfo eventActions'
'timeOptions titles';
grid-template-columns: auto 1fr; 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 { .timeSettings {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
}
.titles { .eventInfo {
grid-area: titles; grid-area: eventInfo;
display: grid; }
grid-template-areas:
'left right'
'tag tag';
grid-template-columns: 1fr 1fr;
.left, .eventActions {
.right { grid-area: eventActions;
display: flex; margin-left: auto;
flex-direction: column; }
gap: 8px;
margin-top: 4px;
}
.left { .timeOptions {
grid-area: left; grid-area: timeOptions;
padding-right: 16px; display: flex;
} gap: 24px;
}
.right { .titles {
padding-left: 16px; grid-area: titles;
grid-area: right; display: grid;
border-left: 1px solid $bg-container-l3; grid-template-areas: 'left right';
} grid-template-columns: 1fr 1fr;
.osc { .left,
grid-area: tag; .right {
justify-self: end;
}
}
.inline {
display: flex;
align-items: center;
gap: 16px;
}
.column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
margin-top: 4px;
} }
.padTop { .left {
margin-top: 8px; grid-area: left;
padding: 0 16px;
border-left: 1px solid $border-color-ondark;
} }
.inputLabel { .right {
font-size: 13px; padding-left: 16px;
display: block; grid-area: right;
color: $label-gray; border-left: 1px solid $border-color-ondark;
.delayLabel {
color: $text-delay;
}
} }
} }
.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 { useInfoPanel } from '../../common/hooks/useSocket';
import InfoTitle from './CollapsableInfo';
import InfoLogger from './InfoLogger'; import InfoLogger from './InfoLogger';
import InfoNif from './InfoNif'; import InfoNif from './InfoNif';
import InfoTitle from './InfoTitle';
import style from './Info.module.scss'; import style from './Info.module.scss';
@@ -31,7 +31,7 @@ export default function Info() {
return ( return (
<> <>
<div className={style.main}> <div className={style.panelHeader}>
<span>Ontime running on port 4001</span> <span>Ontime running on port 4001</span>
<span>{selected}</span> <span>{selected}</span>
</div> </div>
+28 -56
View File
@@ -1,77 +1,49 @@
@use '../../theme/main' as *;
@use '../../theme/mixins' as *; @use '../../theme/mixins' as *;
@use '../../theme/v2Styles' as *;
@mixin container { .panelHeader {
@include second-container; font-size: $inner-section-text-size;
margin-top: 1em; font-family: $ontime-font-family;
padding: 8px;
}
.container {
@include container;
}
.main {
font-size: 0.9em;
color: $label-gray; color: $label-gray;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.collapsedTitle { .container {
font-size: inherit; margin-top: $main-spacing;
padding-left: 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.labelContainer { .labels {
white-space: nowrap; font-size: $inner-section-text-size;
overflow: hidden; display: flex;
text-overflow: ellipsis; flex-direction: column;
} gap: $element-inner-spacing;
.label,
.emptyLabel {
padding: 0;
margin: 0;
font-size: 0.9em;
} }
.label { .label {
font-size: 0.9em; color: $section-white;
color: $label-gray;
} }
.label::after { .content {
content: ': '; color: $secondary-text-gray;
} margin-left: $element-inner-spacing;
font-size: $text-body-size;
.emptyLabel {
font-size: 0.8em;
color: $bg-gray-700;
}
.notes {
color: $notes-color;
overflow: hidden;
text-overflow: ellipsis;
} }
.interfaceList { .interfaceList {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: $element-spacing;
margin-top: 4px;
.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 { 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 ErrorBoundary from '../../common/components/errorBoundary/ErrorBoundary';
import { handleLinks } from '../../common/utils/linkUtils'; import { handleLinks } from '../../common/utils/linkUtils';
@@ -11,7 +11,7 @@ import style from '../editors/Editor.module.scss';
export default function InfoExport() { export default function InfoExport() {
return ( return (
<Box className={style.info} data-testid="panel-info"> <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}> <div className={style.content}>
<ErrorBoundary> <ErrorBoundary>
<Info /> <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/v2Styles' as *;
@use '../../theme/main' as *;
@use '../../theme/mixins' as *; @use '../../theme/mixins' as *;
.container, $info-gray: $secondary-text-gray;
.container__expanded{ $info-hover: $section-white;
@include container;
max-height: 80%;
}
.container__expanded { .infoLoggerContainer {
min-height: 50%; max-height: 80%;
height: 100% margin-top: 32px;
&.expanded {
min-height: 50%;
height: 100%
}
} }
.log { .log {
@@ -18,70 +19,44 @@
overflow-y: scroll; overflow-y: scroll;
font-size: 0.8em; font-size: 0.8em;
user-select: text; user-select: text;
padding: 0 0.5em; }
margin: 0 0.5em;
li { .logEntry {
display: flex; display: flex;
margin-bottom: 2px; margin-bottom: 2px;
&.INFO {
.time {
width: 4.5em
}
.origin {
width: 6em;
}
.msg {
flex: 1;
}
}
li.info {
color: $info-gray; color: $info-gray;
} }
li.warn {
&.WARN {
color: $warning-orange; color: $warning-orange;
} }
li.error {
&.ERROR {
color: $error-red; color: $error-red;
} }
li:hover { &:hover {
color: $info-gray-hover; color: $info-hover;
}
.time {
width: 4.5em
}
.origin {
width: 6em;
}
.msg {
flex: 1;
} }
} }
.info { .buttonBar {
color: $text-white;
}
.error {
color: red;
}
.toggleBar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 8px;
font-size: 0.7em;
justify-content: flex-start; justify-content: flex-start;
padding: 0.5em 8px; margin-bottom: 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);
}
} }
+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 { useState } from 'react';
import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
import CollapseBar from '../../common/components/collapseBar/CollapseBar'; import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import useInfo from '../../common/hooks-query/useInfo'; import useInfo from '../../common/hooks-query/useInfo';
@@ -7,28 +8,32 @@ import { openLink } from '../../common/utils/linkUtils';
import style from './Info.module.scss'; import style from './Info.module.scss';
export default function InfoNif() { export default function InfoNif() {
const { data, status } = useInfo(); const { data } = useInfo();
const [collapsed, setCollapsed] = useState(false); 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 ( return (
<div className={style.container}> <div className={style.container}>
<CollapseBar <CollapseBar
title='Network Info' title='Network Info'
isCollapsed={collapsed} isCollapsed={collapsed}
onClick={() => setCollapsed((c) => !c)} onClick={() => setCollapsed((prev) => !prev)}
/> />
{!collapsed && (status === 'success') &&( {!collapsed && (
<div className={style.interfaceList}> <div className={style.interfaceList}>
{data?.networkInterfaces.map((e) => ( {data?.networkInterfaces.map((nif) => (
<a <span
key={e.address} key={nif.address}
href='#!' onClick={() => handleClick(nif.address)}
onClick={() => openLink(baseURL.replace('__IP__', e.address))}
className={style.interface} className={style.interface}
> >
{`${e.name} - ${e.address}`} {`${nif.name} - ${nif.address}`}
</a> <IoArrowUp className={style.linkIcon} />
</span>
))} ))}
</div> </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 { useCallback, useEffect } from 'react';
import { VStack } from '@chakra-ui/react'; import { VStack } from '@chakra-ui/react';
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle'; 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 { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
import { FiSave } from '@react-icons/all-files/fi/FiSave'; import { FiSave } from '@react-icons/all-files/fi/FiSave';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload'; 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 { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from 'common/api/ontimeApi'; import { downloadRundown } from 'common/api/ontimeApi';
@@ -28,6 +28,12 @@ const buttonStyle = {
fontSize: '1.5em', fontSize: '1.5em',
size: 'lg', size: 'lg',
colorScheme: 'white', 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) { export default function MenuBar(props: MenuBarProps) {
@@ -94,17 +100,17 @@ export default function MenuBar(props: MenuBarProps) {
<QuitIconBtn clickHandler={() => actionHandler('shutdown')} /> <QuitIconBtn clickHandler={() => actionHandler('shutdown')} />
<TooltipActionBtn <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
icon={<FiMaximize />} icon={<IoScan />}
clickHandler={() => actionHandler('max')} clickHandler={() => actionHandler('max')}
tooltip='Show full window' tooltip='Show full window'
aria-label='' aria-label='Show full window'
/> />
<TooltipActionBtn <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
icon={<FiMinimize />} icon={<FiMinimize />}
clickHandler={() => actionHandler('min')} clickHandler={() => actionHandler('min')}
tooltip='Close to tray' tooltip='Minimise to tray'
aria-label='' aria-label='Minimise to tray'
/> />
<div className={style.gap} /> <div className={style.gap} />
<TooltipActionBtn <TooltipActionBtn
@@ -112,7 +118,7 @@ export default function MenuBar(props: MenuBarProps) {
icon={<FiHelpCircle />} icon={<FiHelpCircle />}
clickHandler={() => actionHandler('help')} clickHandler={() => actionHandler('help')}
tooltip='Help' tooltip='Help'
aria-label='' aria-label='Help'
/> />
<TooltipActionBtn <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
@@ -120,8 +126,7 @@ export default function MenuBar(props: MenuBarProps) {
className={isSettingsOpen ? style.open : ''} className={isSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen} clickHandler={onSettingsOpen}
tooltip='Settings' tooltip='Settings'
isRound aria-label='Settings'
aria-label=''
/> />
<div className={style.gap} /> <div className={style.gap} />
<TooltipActionBtn <TooltipActionBtn
@@ -129,16 +134,15 @@ export default function MenuBar(props: MenuBarProps) {
icon={<FiUpload />} icon={<FiUpload />}
className={isUploadOpen ? style.open : ''} className={isUploadOpen ? style.open : ''}
clickHandler={onUploadOpen} clickHandler={onUploadOpen}
tooltip='Upload event list' tooltip='Upload showfile'
isRound aria-label='Upload showfile'
aria-label=''
/> />
<TooltipActionBtn <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
icon={<FiSave />} icon={<FiSave />}
clickHandler={downloadRundown} clickHandler={downloadRundown}
tooltip='Export event list' tooltip='Export showfile'
aria-label='' aria-label='Export showfile'
/> />
</VStack> </VStack>
); );
+22 -30
View File
@@ -1,30 +1,24 @@
import { memo, useCallback, useContext } from 'react'; import { memo, useCallback, useContext } from 'react';
import { import {
Divider, Button,
HStack, HStack,
IconButton,
Menu, Menu,
MenuButton, MenuButton,
MenuDivider,
MenuItem, MenuItem,
MenuList, MenuList,
Switch, Switch,
Tooltip,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle'; 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 { 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 { CursorContext } from 'common/context/CursorContext';
import { useEventAction } from '../../common/hooks/useEventAction'; import { useEventAction } from '../../common/hooks/useEventAction';
import style from './RundownMenu.module.scss'; import style from './RundownMenu.module.scss';
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
};
const RundownMenu = () => { const RundownMenu = () => {
const { isCursorLocked, toggleCursorLocked } = useContext(CursorContext); const { isCursorLocked, toggleCursorLocked } = useContext(CursorContext);
const { addEvent, deleteAllEvents } = useEventAction(); const { addEvent, deleteAllEvents } = useEventAction();
@@ -56,33 +50,31 @@ const RundownMenu = () => {
<Switch <Switch
defaultChecked={isCursorLocked} defaultChecked={isCursorLocked}
onChange={(event) => toggleCursorLocked(event.target.checked)} onChange={(event) => toggleCursorLocked(event.target.checked)}
colorScheme='blue' variant='ontime'
/> />
Lock cursor to current Lock cursor to current
</label> </label>
<Menu isLazy lazyBehavior='unmount'> <Menu isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
<Tooltip label='Add / Delete ...'> <MenuButton
<MenuButton as={Button}
as={IconButton} leftIcon={<IoAdd />}
aria-label='Create Menu' size='sm'
size='sm' variant='ontime-subtle'
icon={<FiPlus />} >
colorScheme='white' Add event
variant='outline' </MenuButton>
/> <MenuList>
</Tooltip> <MenuItem icon={<IoAdd />} onClick={() => eventAction('event')}>
<MenuList style={menuStyle}> Event at start
<MenuItem icon={<FiPlus />} onClick={() => eventAction('event')}>
Add Event at start
</MenuItem> </MenuItem>
<MenuItem icon={<FiClock />} onClick={() => eventAction('delay')}> <MenuItem icon={<IoTimerOutline />} onClick={() => eventAction('delay')}>
Add Delay at start Delay at start
</MenuItem> </MenuItem>
<MenuItem icon={<FiMinusCircle />} onClick={() => eventAction('block')}> <MenuItem icon={<FiMinusCircle />} onClick={() => eventAction('block')}>
Add Block at start Block at start
</MenuItem> </MenuItem>
<Divider /> <MenuDivider />
<MenuItem icon={<FiTrash2 />} onClick={() => eventAction('delete-all')} color='red.500'> <MenuItem icon={<FiTrash2 />} onClick={() => eventAction('delete-all')} color='#D20300'>
Delete All Delete All
</MenuItem> </MenuItem>
</MenuList> </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 { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Button } from '@chakra-ui/react'; import { Button } from '@chakra-ui/react';
import { import {
@@ -14,8 +14,6 @@ import { duplicateEvent } from 'common/utils/eventsManager';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useSubscription from '../../common/hooks/useSubscription';
import QuickAddBlock from './quick-add-block/QuickAddBlock'; import QuickAddBlock from './quick-add-block/QuickAddBlock';
import RundownEntry from './RundownEntry'; import RundownEntry from './RundownEntry';
@@ -32,8 +30,6 @@ export default function Rundown(props) {
const { addEvent, reorderEvent } = useEventAction(); const { addEvent, reorderEvent } = useEventAction();
const cursorRef = createRef(); const cursorRef = createRef();
const showQuickEntry = useAtomValue(showQuickEntryAtom); const showQuickEntry = useAtomValue(showQuickEntryAtom);
const [selectedId] = useSubscription('selected-id', null);
const [nextId] = useSubscription('next-id', null);
const insertAtCursor = useCallback( const insertAtCursor = useCallback(
(type, cursor) => { (type, cursor) => {
@@ -83,20 +79,17 @@ export default function Rundown(props) {
if (event.keyCode === 38) { if (event.keyCode === 38) {
if (cursor > 0) moveCursorUp(); if (cursor > 0) moveCursorUp();
} }
// E if (event.code === 'KeyE') {
if (event.code === "KeyE") {
event.preventDefault(); event.preventDefault();
if (cursor == null) return; if (cursor == null) return;
insertAtCursor('event', cursor); insertAtCursor('event', cursor);
} }
// D if (event.code === 'KeyD') {
if (event.code === "KeyD") {
event.preventDefault(); event.preventDefault();
if (cursor == null) return; if (cursor == null) return;
insertAtCursor('delay', cursor); insertAtCursor('delay', cursor);
} }
// B if (event.code === 'KeyB') {
if (event.code === "KeyB") {
event.preventDefault(); event.preventDefault();
if (cursor == null) return; if (cursor == null) return;
insertAtCursor('block', cursor); insertAtCursor('block', cursor);
@@ -166,7 +159,7 @@ export default function Rundown(props) {
[reorderEvent], [reorderEvent],
); );
if (entries.length < 1) { if (!entries.length) {
return ( return (
<div className={style.alignCenter}> <div className={style.alignCenter}>
<Empty text='No data yet' style={{ marginTop: '7vh' }} /> <Empty text='No data yet' style={{ marginTop: '7vh' }} />
@@ -209,25 +202,19 @@ export default function Rundown(props) {
} }
const isLast = index === entries.length - 1; const isLast = index === entries.length - 1;
return ( return (
<div <Fragment key={entry.id}>
key={entry.id} <div ref={cursor === index ? cursorRef : undefined}>
className={`${style.bgElement}
${entry.type === 'event' && cumulativeDelay !== 0 ? style.delayed : ''}`}
>
<div
ref={cursor === index ? cursorRef : undefined}
className={cursor === index ? style.cursor : ''}
>
<RundownEntry <RundownEntry
type={entry.type} type={entry.type}
index={index} index={index}
eventIndex={eventIndex} eventIndex={eventIndex}
data={entry} data={entry}
selected={selectedId === entry.id} selected={data.selectedEventId === entry.id}
next={nextId === entry.id} hasCursor={cursor === index}
next={data.nextEventId === entry.id}
delay={cumulativeDelay} delay={cumulativeDelay}
previousEnd={previousEnd} previousEnd={previousEnd}
playback={selectedId === entry.id ? data.playback : undefined} playback={data.selectedEventId === entry.id ? data.playback : undefined}
/> />
</div> </div>
{((showQuickEntry && index === cursor) || isLast) && ( {((showQuickEntry && index === cursor) || isLast) && (
@@ -239,7 +226,7 @@ export default function Rundown(props) {
disableAddBlock={entry.type === 'block'} disableAddBlock={entry.type === 'block'}
/> />
)} )}
</div> </Fragment>
); );
})} })}
{provided.placeholder} {provided.placeholder}
@@ -20,17 +20,6 @@
align-self: center; align-self: center;
} }
.cursor {
border-radius: 3px;
outline: 1px solid $action-blue;
}
.bgElement {
&.delayed {
background: rgba($block-delay-color, 0.3);
}
}
.alignCenter { .alignCenter {
text-align: center; text-align: center;
flex-direction: column; flex-direction: column;
+4 -2
View File
@@ -32,6 +32,7 @@ interface RundownEntryProps {
eventIndex: number; eventIndex: number;
data: OntimeRundownEntry; data: OntimeRundownEntry;
selected: boolean; selected: boolean;
hasCursor: boolean;
next: boolean; next: boolean;
delay: number; delay: number;
previousEnd: number; previousEnd: number;
@@ -39,7 +40,7 @@ interface RundownEntryProps {
} }
export default function RundownEntry(props: 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 { emitError } = useContext(LoggingContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom); const defaultPublic = useAtomValue(defaultPublicAtom);
@@ -53,7 +54,7 @@ export default function RundownEntry(props: RundownEntryProps) {
value: unknown; value: unknown;
} }
const actionHandler = useCallback( const actionHandler = useCallback(
(action: EventItemActions, payload: number | FieldValue) => { (action: EventItemActions, payload?: number | FieldValue) => {
switch (action) { switch (action) {
case 'set-cursor': { case 'set-cursor': {
moveCursorTo(payload as number); moveCursorTo(payload as number);
@@ -151,6 +152,7 @@ export default function RundownEntry(props: RundownEntryProps) {
next={next} next={next}
skip={data.skip} skip={data.skip}
selected={selected} selected={selected}
hasCursor={hasCursor}
playback={playback} playback={playback}
actionHandler={actionHandler} actionHandler={actionHandler}
/> />
@@ -1,5 +1,5 @@
import { Box } from '@chakra-ui/react'; 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 ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { CursorProvider } from 'common/context/CursorContext'; import { CursorProvider } from 'common/context/CursorContext';
import { handleLinks } from 'common/utils/linkUtils'; import { handleLinks } from 'common/utils/linkUtils';
@@ -12,7 +12,7 @@ export default function RundownExport() {
return ( return (
<CursorProvider> <CursorProvider>
<Box className={style.editor} data-testid='panel-rundown'> <Box className={style.editor} data-testid='panel-rundown'>
<FiArrowUpRight <IoArrowUp
className={style.corner} className={style.corner}
onClick={(event) => handleLinks(event, 'rundown')} onClick={(event) => handleLinks(event, 'rundown')}
/> />
+21 -19
View File
@@ -1,31 +1,33 @@
@use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *;
$block-gap: 4px; $block-gap: 4px;
$element-spacing: 4px; $block-element-spacing: 4px;
$binder-width: 32px; $block-binder-width: 32px;
$clearance: 8px; $block-clearance: 8px;
$block-border-radius: 3px; $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() { @mixin block-spacing() {
padding: 4px 10px 4px 2px; padding: 4px 10px 4px 2px;
gap: 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() { @mixin drag-style() {
font-size: 20px; font-size: 20px;
text-align: center; justify-self: center;
opacity: 0.3; opacity: 0.3;
cursor: grab; cursor: grab;
transition: opacity 0.3s; 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 *; @use '../blockMixins' as *;
/* ============= COMMON ============= */
.block { .block {
@include block-spacing; @include block-spacing;
@include block-styling;
box-sizing: content-box; box-sizing: content-box;
display: grid; display: grid;
grid-template-columns: 40px 1fr; grid-template-columns: 32px 1fr;
align-items: center; align-items: center;
height: 40px; height: $secondary-block-height;
border-radius: 2px;
border: $border-l3;
border-bottom: 4px solid $block-block-color;
background-color: $bg-container-l2;
} }
/* ================ DRAG ================ */
.drag { .drag {
@include drag-style; @include drag-style;
color: $block-block-color;
} }
/* ============== ACTION ================ */
.actionOverlay { .actionOverlay {
@include action-overlay; justify-self: flex-end;
justify-self: end;
}
.block:hover > .actionOverlay {
@include block-hover;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
} }
@@ -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 *; @use '../blockMixins' as *;
/* ============= COMMON ============= */
.delay { .delay {
@include block-spacing; @include block-spacing;
@include block-styling;
box-sizing: content-box;
display: grid; display: grid;
grid-template-columns: 32px 1fr auto; grid-template-columns: 32px 1fr auto;
grid-template-areas: 'drag inpt btns'; grid-template-areas: 'drag inpt btns';
align-items: center; align-items: center;
height: 40px; height: $secondary-block-height;
border-radius: 2px; gap: 8px;
border: $border-l3;
border-top: 4px solid $block-delay-color;
background-color: $bg-container-l2;
} }
/* ================ DRAG ================ */
.drag { .drag {
grid-area: drag;
@include drag-style; @include drag-style;
color: $block-delay-color; grid-area: drag;
}
/* =============== INPUT ================ */
.input {
grid-area: inpt;
}
/* ============== ACTION ================ */
.actionOverlay {
@include action-overlay;
}
.delay:hover > .actionOverlay {
@include block-hover;
}
/* NA */
.actionOverlay:hover {
opacity: 1;
} }
@@ -1,32 +1,34 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { Button, HStack } from '@chakra-ui/react'; import { Button, HStack } from '@chakra-ui/react';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck'; import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import ActionButtons from 'common/components/buttons/ActionButtons'; import DelayInput from 'common/components/input/delay-input/DelayInput';
import TooltipLoadingActionBtn from 'common/components/buttons/TooltipLoadingActionBtn';
import DelayInput from 'common/components/input/DelayInput';
import { useEventAction } from 'common/hooks/useEventAction'; import { useEventAction } from 'common/hooks/useEventAction';
import { millisToMinutes } from 'common/utils/dateConfig'; 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'; 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 { data, index, actionHandler } = props;
const { applyDelay, deleteEvent, updateEvent } = useEventAction(); const { applyDelay, updateEvent } = useEventAction();
const applyDelayHandler = useCallback(() => { const applyDelayHandler = useCallback(() => {
applyDelay(data.id); applyDelay(data.id);
}, [data.id, applyDelay]); }, [data.id, applyDelay]);
const deleteHandler = useCallback(() => {
deleteEvent(data.id);
}, [data.id, deleteEvent]);
const delaySubmitHandler = useCallback( const delaySubmitHandler = useCallback(
(value) => { (value: number) => {
const newEvent = { const newEvent = {
id: data.id, id: data.id,
duration: value * 60000, duration: value * 60000,
@@ -34,7 +36,7 @@ export default function DelayBlock(props) {
updateEvent(newEvent); updateEvent(newEvent);
}, },
[data.id, updateEvent] [data.id, updateEvent],
); );
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined; const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
@@ -47,39 +49,22 @@ export default function DelayBlock(props) {
<IoReorderTwo /> <IoReorderTwo />
</span> </span>
<DelayInput <DelayInput
className={style.input}
value={delayValue} value={delayValue}
submitHandler={delaySubmitHandler} submitHandler={delaySubmitHandler}
/> />
<HStack spacing='4px' className={style.actionOverlay}> <HStack spacing='8px' className={style.actionOverlay}>
<Button <Button
onClick={applyDelayHandler} onClick={applyDelayHandler}
size='sm' size='sm'
color="#F57C13" leftIcon={<IoCheckmark />}
borderColor="#F57C13" variant='ontime-subtle-white'
leftIcon={<FiCheck />}
variant='outline'
> >
Apply delay Apply delay
</Button> </Button>
<TooltipLoadingActionBtn <BlockActionMenu showAdd enableDelete actionHandler={actionHandler} />
clickHandler={deleteHandler}
icon={<IoRemove />}
tooltip='Delete'
variant='outline'
colorScheme='white'
size='sm'
/>
<ActionButtons showAdd actionHandler={actionHandler} />
</HStack> </HStack>
</div> </div>
)} )}
</Draggable> </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 *; @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 { .statusElements {
grid-area: estatus; grid-area: estatus;
display: grid; display: grid;
@@ -9,189 +133,40 @@
"progb progb"; "progb progb";
gap: 2px; gap: 2px;
grid-template-rows: auto 4px; 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; 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%; height: 100%;
padding: 2px 0;
} }
.progressBg.hidden { .eventNote {
opacity: 0; 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 { FiUsers } from '@react-icons/all-files/fi/FiUsers';
import { IoOptions } from '@react-icons/all-files/io5/IoOptions'; import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; 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 { 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 { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle'; import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline'; import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; 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 { IoTimerOutline } from '@react-icons/all-files/io5/IoTimerOutline';
import { editorEventId } from 'common/atoms/LocalEventSettings'; import { editorEventId } from 'common/atoms/LocalEventSettings';
import TooltipActionBtn from 'common/components/buttons/TooltipActionBtn'; 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 { useAtom } from 'jotai';
import { useEventAction } from '../../../common/hooks/useEventAction'; import { useEventAction } from '../../../common/hooks/useEventAction';
@@ -23,7 +22,7 @@ import { Playstate } from '../../../common/models/OntimeTypes';
import { tooltipDelayMid } from '../../../ontimeConfig'; import { tooltipDelayMid } from '../../../ontimeConfig';
import { EventItemActions } from '../RundownEntry'; import { EventItemActions } from '../RundownEntry';
import EventBlockActionMenu from './composite/EventBlockActionMenu'; import BlockActionMenu from './composite/BlockActionMenu';
import EventBlockProgressBar from './composite/EventBlockProgressBar'; import EventBlockProgressBar from './composite/EventBlockProgressBar';
import EventBlockTimers from './composite/EventBlockTimers'; import EventBlockTimers from './composite/EventBlockTimers';
@@ -31,9 +30,6 @@ import style from './EventBlock.module.scss';
const blockBtnStyle = { const blockBtnStyle = {
size: 'sm', size: 'sm',
colorScheme: 'white',
variant: 'ghost',
fontSize: '20px',
}; };
const tooltipProps = { const tooltipProps = {
@@ -56,8 +52,9 @@ interface EventBlockProps {
next: boolean; next: boolean;
skip: boolean; skip: boolean;
selected: boolean; selected: boolean;
hasCursor: boolean;
playback?: Playstate; playback?: Playstate;
actionHandler: (action: EventItemActions, payload: any) => void; actionHandler: (action: EventItemActions, payload?: any) => void;
} }
export default function EventBlock(props: EventBlockProps) { export default function EventBlock(props: EventBlockProps) {
@@ -77,6 +74,7 @@ export default function EventBlock(props: EventBlockProps) {
next, next,
skip = false, skip = false,
selected, selected,
hasCursor,
playback, playback,
actionHandler, actionHandler,
} = props; } = props;
@@ -89,6 +87,7 @@ export default function EventBlock(props: EventBlockProps) {
const hasDelay = delay !== 0 && delay !== null; const hasDelay = delay !== 0 && delay !== null;
// Todo: could I re-render the item without causing a state change here? // Todo: could I re-render the item without causing a state change here?
// ?? use refs instead?
useEffect(() => { useEffect(() => {
setBlockTitle(title); setBlockTitle(title);
}, [title]); }, [title]);
@@ -115,13 +114,18 @@ export default function EventBlock(props: EventBlockProps) {
playBtnStyles._hover = {}; playBtnStyles._hover = {};
} }
const blockClasses = cx([
style.eventBlock,
skip ? style.skip : null,
selected ? style.selected : null,
hasCursor ? style.hasCursor : null,
]);
return ( return (
<Draggable key={eventId} draggableId={eventId} index={index}> <Draggable key={eventId} draggableId={eventId} index={index}>
{(provided) => ( {(provided) => (
<div <div
className={`${style.eventBlock} ${skip ? style.skip : ''} ${ className={blockClasses}
selected ? style.selected : ''
}`}
{...provided.draggableProps} {...provided.draggableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
@@ -138,36 +142,38 @@ export default function EventBlock(props: EventBlockProps) {
</div> </div>
<div className={style.playbackActions}> <div className={style.playbackActions}>
<TooltipActionBtn <TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Skip event' aria-label='Skip event'
tooltip='Skip event' tooltip='Skip event'
openDelay={tooltipDelayMid} openDelay={tooltipDelayMid}
icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />} icon={skip ? <IoRemoveCircle /> : <IoRemoveCircleOutline />}
{...blockBtnStyle} {...blockBtnStyle}
variant={skip ? 'solid' : 'ghost'}
clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })} clickHandler={() => actionHandler('update', { field: 'skip', value: !skip })}
tabIndex={-1} tabIndex={-1}
disabled={selected} disabled={selected}
/> />
<TooltipActionBtn <TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Load event' aria-label='Load event'
tooltip='Load event' tooltip='Load event'
openDelay={tooltipDelayMid} openDelay={tooltipDelayMid}
icon={selected ? <IoPlayBackOutline /> : <IoReload />} icon={<IoReload className={style.flip} />}
disabled={skip} disabled={skip}
{...blockBtnStyle} {...blockBtnStyle}
clickHandler={() => setEventPlayback.loadEvent(eventId)} clickHandler={() => setEventPlayback.loadEvent(eventId)}
tabIndex={-1} tabIndex={-1}
/> />
<TooltipActionBtn <TooltipActionBtn
variant='ontime-subtle-white'
aria-label='Start event' aria-label='Start event'
tooltip='Start event' tooltip='Start event'
openDelay={tooltipDelayMid} openDelay={tooltipDelayMid}
icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />} icon={eventIsPlaying ? <IoPlay /> : <IoPlayOutline />}
disabled={skip} disabled={skip}
{...blockBtnStyle} {...blockBtnStyle}
variant={eventIsPlaying ? 'solid' : 'ghost'}
clickHandler={() => setEventPlayback.startEvent(eventId)} clickHandler={() => setEventPlayback.startEvent(eventId)}
backgroundColor={eventIsPlaying ? '#58A151' : undefined} backgroundColor={eventIsPlaying ? '#58A151' : undefined}
_hover={{backgroundColor: eventIsPlaying ? '#58A151' : undefined}}
tabIndex={-1} tabIndex={-1}
/> />
</div> </div>
@@ -186,7 +192,7 @@ export default function EventBlock(props: EventBlockProps) {
onChange={(value) => setBlockTitle(value)} onChange={(value) => setBlockTitle(value)}
onSubmit={(value) => handleTitle(value)} onSubmit={(value) => handleTitle(value)}
> >
<EditablePreview className={style.eventTitle__preview} /> <EditablePreview className={style.preview} />
<EditableInput /> <EditableInput />
</Editable> </Editable>
<div className={style.statusElements}> <div className={style.statusElements}>
@@ -195,54 +201,53 @@ export default function EventBlock(props: EventBlockProps) {
<EventBlockProgressBar playback={playback} /> <EventBlockProgressBar playback={playback} />
</div> </div>
<div className={style.eventStatus}> <div className={style.eventStatus}>
<Tooltip label='Next event' isDisabled={!next} {...tooltipProps}> <Tooltip
<span label='Next event'
className={`${style.statusIcon} ${style.statusNext} ${next ? style.enabled : ''}`} isDisabled={!next}
shouldWrapChildren {...tooltipProps}
> >
<IoReturnDownForward /> <IoPlaySkipForward
</span> className={`${style.statusIcon} ${style.statusNext} ${next ? style.enabled : ''}`} />
</Tooltip> </Tooltip>
<Tooltip label='Event has delay' isDisabled={!hasDelay} {...tooltipProps}> <Tooltip
<span label='Event has delay'
className={`${style.statusIcon} ${style.statusDelay} ${ isDisabled={!hasDelay}
hasDelay ? style.enabled : '' shouldWrapChildren {...tooltipProps}
}`}
> >
<IoTimerOutline /> <IoTimerOutline className={`${style.statusIcon} ${style.statusDelay} ${
</span> hasDelay ? style.enabled : ''
}`} />
</Tooltip> </Tooltip>
<Tooltip <Tooltip
label={`${isPublic ? 'Event is public' : 'Event is private'}`} label={`${isPublic ? 'Event is public' : 'Event is private'}`}
{...tooltipProps} {...tooltipProps}
shouldWrapChildren
> >
<span <FiUsers className={`${style.statusIcon} ${style.statusPublic} ${
className={`${style.statusIcon} ${style.statusPublic} ${
isPublic ? style.enabled : '' isPublic ? style.enabled : ''
}`} }`} />
>
<FiUsers />
</span>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<div className={style.eventActions}> <div className={style.eventActions}>
<TooltipActionBtn <TooltipActionBtn
{...blockBtnStyle} {...blockBtnStyle}
variant='ontime-subtle-white'
size='sm' size='sm'
icon={<IoOptions />} icon={<IoOptions />}
clickHandler={() => setOpenId((prev) => prev === eventId ? null : eventId)} clickHandler={() => setOpenId((prev) => prev === eventId ? null : eventId)}
tooltip='Event options' tooltip='Event options'
aria-label='Event options' aria-label='Event options'
tabIndex={-1} tabIndex={-1}
backgroundColor={openId === eventId ? '#ebedf0' : 'transparent'} backgroundColor={openId === eventId ? '#2B5ABC' : undefined}
color={openId === eventId ? '#333' : '#ebedf0'} color={openId === eventId ? 'white' : '#f6f6f6'}
_hover={{ bg: '#ebedf0', color: '#333' }}
/> />
<EventBlockActionMenu <BlockActionMenu
showAdd showAdd
showDelay showDelay
showBlock showBlock
showClone showClone
enableDelete
actionHandler={actionHandler} actionHandler={actionHandler}
/> />
</div> </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 { 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 { LoggingContext } from 'common/context/LoggingContext';
import { millisToMinutes } from 'common/utils/dateConfig'; import { millisToMinutes } from 'common/utils/dateConfig';
import { stringFromMillis } from 'common/utils/time'; import { stringFromMillis } from 'common/utils/time';
@@ -1,85 +1,45 @@
@use '../../../theme/main' as *; @use '../../../theme/main' as *;
.quickAdd { .quickAdd {
background-color: $bg-container-l2; display: grid;
display: flex; grid-template-columns: 1fr auto;
align-items: center; align-items: center;
justify-content: center; margin: 4px 2px;
gap: 5%;
margin: 4px 2px 2px;
font-size: 12px; font-size: 12px;
}
.createEvent, .btnRow {
.createDelay, justify-self: center;
.createBlock { display: flex;
gap: 10%;
.quickBtn {
width: auto; width: auto;
padding: 0 16px; 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 { .createEvent {
border: 1px solid $light-bg; color: $light-bg;
color: lighten($light-bg, 30%);
&:hover {
background-color: $light-bg;
opacity: 1;
}
} }
.createDelay { .createDelay {
border: 1px solid $block-delay-color; color: $block-delay-color;
color: lighten($block-delay-color, 30%);
&:hover {
background-color: $block-delay-color;
opacity: 1;
}
} }
.createBlock { .createBlock {
border: 1px solid $block-block-color; color: $block-block-color;
color: lighten($block-block-color, 30%);
&:hover {
background-color: $block-block-color;
opacity: 1;
}
} }
} }
.keyboard { .keyboard {
margin-left: 4px; margin-left: 8px;
padding: 0 4px; padding: 0 4px;
color: $label-gray; color: $label-gray;
border-radius: 2px; border-radius: 2px;
background-color: rgba(0, 0, 0, 0.24); background-color: rgba(0, 0, 0, 0.1);
} }
.options { .options {
display: flex; display: flex;
flex-direction: column; 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 { 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 { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings';
import { LoggingContext } from 'common/context/LoggingContext'; import { LoggingContext } from 'common/context/LoggingContext';
import { useEventAction } from 'common/hooks/useEventAction'; import { useEventAction } from 'common/hooks/useEventAction';
@@ -31,7 +31,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom); const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom); const defaultPublic = useAtomValue(defaultPublicAtom);
const doStartTime = useRef<HTMLInputElement | null>(null); const doStartTime = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null) const doPublic = useRef<HTMLInputElement | null>(null);
const handleCreateEvent = useCallback((eventType: EventTypes) => { const handleCreateEvent = useCallback((eventType: EventTypes) => {
switch (eventType) { switch (eventType) {
@@ -62,38 +62,45 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
return ( return (
<div className={style.quickAdd}> <div className={style.quickAdd}>
<Tooltip label='Add Event' openDelay={tooltipDelayMid}> <div className={style.btnRow}>
<span <Tooltip label='Add Event' openDelay={tooltipDelayMid}>
className={style.createEvent} <Button
onClick={() => handleCreateEvent('event')} onClick={() => handleCreateEvent('event')}
role='button' size='xs'
> variant='ontime-subtle'
E{showKbd && <span className={style.keyboard}>Alt + E</span>} className={`${style.quickBtn} ${style.createBlock}`}
</span> >
</Tooltip> E{showKbd && <span className={style.keyboard}>Alt + E</span>}
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}> </Button>
<span </Tooltip>
className={`${style.createDelay} ${disableAddDelay ? style.disabled : ''}`} <Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
onClick={() => handleCreateEvent('delay')} <Button
role='button' onClick={() => handleCreateEvent('delay')}
> size='xs'
D{showKbd && <span className={style.keyboard}>Alt + D</span>} variant='ontime-subtle'
</span> disabled={disableAddDelay}
</Tooltip> className={`${style.quickBtn} ${style.createDelay}`}
<Tooltip label='Add Block' openDelay={tooltipDelayMid}> >
<span D{showKbd && <span className={style.keyboard}>Alt + D</span>}
className={`${style.createBlock} ${disableAddBlock ? style.disabled : ''}`} </Button>
onClick={() => handleCreateEvent('block')} </Tooltip>
role='button' <Tooltip label='Add Block' openDelay={tooltipDelayMid}>
> <Button
B{showKbd && <span className={style.keyboard}>Alt + B</span>} onClick={() => handleCreateEvent('block')}
</span> size='xs'
</Tooltip> 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}> <div className={style.options}>
<Checkbox <Checkbox
ref={doStartTime} ref={doStartTime}
size='sm' size='sm'
colorScheme='blue' variant='ontime-ondark'
defaultChecked={startTimeIsLastEnd} defaultChecked={startTimeIsLastEnd}
> >
Start time is last end Start time is last end
@@ -101,7 +108,7 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
<Checkbox <Checkbox
ref={doPublic} ref={doPublic}
size='sm' size='sm'
colorScheme='blue' variant='ontime-ondark'
defaultChecked={defaultPublic} defaultChecked={defaultPublic}
> >
Event is public Event is public
@@ -82,7 +82,6 @@ export default function OntimeTable({ tableData, userFields, selectedId, handleU
); );
const handleResetReordering = useCallback(() => { const handleResetReordering = useCallback(() => {
console.log('called reset', defaultColumnOrder)
saveColumnOrder(defaultColumnOrder); saveColumnOrder(defaultColumnOrder);
setColumnOrder(defaultColumnOrder); setColumnOrder(defaultColumnOrder);
}, [saveColumnOrder, setColumnOrder]); }, [saveColumnOrder, setColumnOrder]);
+4 -66
View File
@@ -194,11 +194,11 @@
} }
$bg-theme-light: #fcfcfc; $bg-theme-light: #fcfcfc;
$cell-theme-light: #f6f6f6; $cell-theme-light: #ececec;
$text-theme-light: #202020; $text-theme-light: #202020;
$bg-theme-dark: #121212; $bg-theme-dark: #121212;
$bg2-theme-dark: #1c1c1c; $bg2-theme-dark: #1c1c1c;
$cell-theme-dark: #323232; $cell-theme-dark: #2d2d2d;
$text-theme-dark: white; $text-theme-dark: white;
.tableWrapper { .tableWrapper {
@@ -221,6 +221,7 @@ $text-theme-dark: white;
td { td {
background-color: $cell-theme-light; background-color: $cell-theme-light;
border: 1px solid $bg-theme-light; border: 1px solid $bg-theme-light;
color: #121212;
} }
.actionText:hover, .actionText:hover,
.actionIcon:hover, .actionIcon:hover,
@@ -242,6 +243,7 @@ $text-theme-dark: white;
td { td {
background-color: $cell-theme-dark; background-color: $cell-theme-dark;
border: 1px solid $bg-theme-dark; border: 1px solid $bg-theme-dark;
color: #ececec;
} }
.actionText:hover, .actionText:hover,
.actionIcon:hover, .actionIcon:hover,
@@ -306,67 +308,3 @@ svg {
transform: rotate(360deg); 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 { 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 { TableSettingsContext } from 'common/context/TableSettingsContext';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -37,14 +37,13 @@ useEffect(() => {
return ( return (
<AutoTextArea <AutoTextArea
size='sm' size='sm'
borderColor='#0001'
value={value} value={value}
onChange={onChange} onChange={onChange}
onBlur={onBlur} onBlur={onBlur}
rows={3} rows={3}
transition='none' transition='none'
spellCheck={false} 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) { export default function EventRow(props) {
const { row, index, selectedId, delay } = props; const { row, index, selectedId, delay } = props;
const selected = row.original.id === selectedId; const selected = row.original.id === selectedId;
const colours = getAccessibleColour(row.original.colour);
const colours = row.original.colour
? getAccessibleColour(row.original.colour)
: {};
return ( return (
<tr {...row.getRowProps()} className={selected ? style.selected : ''} id={row.original.id}> <tr {...row.getRowProps()} className={selected ? style.selected : ''} id={row.original.id}>
<td className={style.indexColumn}>{index}</td> <td className={style.indexColumn}>{index}</td>
{row.cells.map((cell) => { {row.cells.map((cell) => {
const { key, style, ...restCellProps } = cell.getCellProps(); const { key, style, ...restCellProps } = cell.getCellProps();
const dynamicStyles = const dynamicStyles = { ...style, ...colours };
selected &&
(cell.column.Header === 'Start' ||
cell.column.Header === 'End' ||
cell.column.Header === 'Duration' ||
cell.column.Header === 'Public')
? { ...style }
: { ...style, ...colours };
// Inject delay value if exits // Inject delay value if exits
if (delay !== 0 && delay != null) { if (delay !== 0 && delay != null) {
+2 -3
View File
@@ -1,4 +1,3 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import App from './App'; import App from './App';
@@ -9,7 +8,7 @@ const container = document.getElementById('root');
const root = createRoot(container); const root = createRoot(container);
root.render( root.render(
<StrictMode> // <StrictMode>
<App /> <App />
</StrictMode> // </StrictMode>
); );
+8 -6
View File
@@ -1,4 +1,4 @@
@use './theme/main'; @use './theme/v2Styles' as *;
* { * {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -10,6 +10,7 @@
body, body,
html, html,
.App { .App {
font-family: $ontime-font-family;
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
overflow: clip; overflow: clip;
@@ -19,14 +20,15 @@ html,
-webkit-app-region: drag; -webkit-app-region: drag;
} }
// workaround for chakra
// // https://github.com/chakra-ui/chakra-ui/issues/417
option { option {
color: black; color: initial;
} }
/* width */ /* width */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.5rem; width: 6px;
height: 0.5rem;
} }
/* Track */ /* Track */
@@ -37,11 +39,11 @@ option {
/* Handle */ /* Handle */
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15); background: #2d2d2d;
border-radius: 2px; border-radius: 2px;
} }
/* Handle on hover */ /* Handle on hover */
::-webkit-scrollbar-thumb: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-action: 0.1s;
$transition-time-feedback: 0.3s; $transition-time-feedback: 0.3s;
@@ -111,15 +113,8 @@ textarea {
} }
// Define style for a link // Define style for a link
a { a:hover {
&::after { color: $ontime-pink;
content: ' \2197';
color: $ontime-pink;
}
&:hover {
color: $ontime-pink;
}
} }
// horizontal separator // horizontal separator
+18 -4
View File
@@ -1,10 +1,10 @@
@use "main" as *; @use "v2Styles" as *;
//////////////////////////////////// general app elements //////////////////////////////////// general app elements
@mixin main-container { @mixin main-container {
background-color: $bg-gray-1000; background-color: $bg-container-l1;
border: 1px solid $bg-gray-1000; border: 1px solid $bg-container-l1;
border-radius: 4px; border-radius: 4px;
} }
@@ -14,7 +14,21 @@
} }
@mixin third-container { @mixin third-container {
background-color: $bg-gray-1100; background-color: $bg-container-l3;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 2px; 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 'main' as *;
@use 'viewerCommon' as *; @use 'viewerCommon' as *;
// Text
$ontime-font-family: "Open Sans", "Segoe UI", sans-serif; // --font-family-override
// General styling // General styling
$accent-color: $ontime-pink; // --accent-color-override $accent-color: $ontime-pink; // --accent-color-override
$delay-color: $delay-text; $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": { "settings": {
"app": "ontime", "app": "ontime",
"version": 1, "version": 2,
"serverPort": 4001, "serverPort": 4001,
"lock": null, "lock": null,
"pinCode": "1234" "pinCode": "1234"
+5 -1
View File
@@ -30,7 +30,11 @@ let isQuitting = false;
(async () => { (async () => {
try { 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; await dbLoader.promise;
const { startServer, startOSCServer } = await import(nodePath); const { startServer, startOSCServer } = await import(nodePath);

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