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