feat: app settings (#658)

* feat: app settings

* refactor: cleanup routes

* style: smaller base font

* chore: migrate about modal

* fix: import links
This commit is contained in:
Carlos Valente
2023-12-25 21:25:09 +01:00
committed by GitHub
parent 6ffc314513
commit a463cb491b
24 changed files with 666 additions and 184 deletions
@@ -0,0 +1,20 @@
@use '../../../theme/v2Styles' as *;
@use '../../../theme/ontimeColours' as *;
.link {
display: flex;
align-items: center;
gap: 0.25rem;
color: $blue-500;
transition-property: color;
transition-duration: $transition-time-action;
&.inline {
display: inline-flex;
}
&:hover {
color: $ontime-color;
}
}
@@ -0,0 +1,29 @@
import { MouseEvent, ReactNode } from 'react';
import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
import { openLink } from '../../utils/linkUtils';
import { cx } from '../../utils/styleUtils';
import style from './ExternalLink.module.scss';
interface ExternalLinkProps {
href: string;
children: ReactNode;
inline?: boolean;
}
export default function ModalLink(props: ExternalLinkProps) {
const { href, inline, children } = props;
const classes = cx([style.link, inline ? style.inline : null]);
const handleClick = (event: MouseEvent) => {
event.preventDefault();
openLink(href);
};
return (
<a href='#!' target='_blank' rel='noreferrer' className={classes} onClick={handleClick}>
{children} <IoOpenOutline />
</a>
);
}
+1
View File
@@ -1,4 +1,5 @@
export const githubUrl = 'https://www.github.com/cpvalente/ontime';
export const apiRepoLatest = 'https://api.github.com/repos/cpvalente/ontime/releases/latest';
export const websiteUrl = 'https://www.getontime.no';
export const gitbookUrl = 'https://ontime.gitbook.io';
@@ -0,0 +1,13 @@
@use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *;
.container {
grid-area: main;
width: 100%;
padding: 1rem;
display: flex;
gap: 0.25rem;
overflow: hidden;
}
@@ -0,0 +1,29 @@
import { ErrorBoundary } from '@sentry/react';
import { useKeyDown } from '../../common/hooks/useKeyDown';
import AboutPanel from './panel/about-panel/AboutPanel';
import PanelContent from './panel-content/PanelContent';
import PanelList from './panel-list/PanelList';
import { useSettingsStore } from './settingsStore';
import style from './AppSettings.module.scss';
export default function AppSettings() {
const setShowSettings = useSettingsStore((state) => state.setShowSettings);
const selectedPanel = useSettingsStore((state) => state.showSettings);
const closeSettings = () => {
setShowSettings(null);
};
useKeyDown(closeSettings, 'Escape');
return (
<div className={style.container}>
<ErrorBoundary>
<PanelList />
<PanelContent onClose={closeSettings}>{selectedPanel === 'about' && <AboutPanel />}</PanelContent>
</ErrorBoundary>
</div>
);
}
@@ -0,0 +1,20 @@
.corner {
position: absolute;
top: 1rem;
right: 1rem;
}
.contentWrapper {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
position: relative;
}
.content {
margin: 1rem;
overflow-y: auto;
flex-grow: 1;
}
@@ -0,0 +1,22 @@
import { PropsWithChildren } from 'react';
import { IconButton } from '@chakra-ui/react';
import { IoClose } from '@react-icons/all-files/io5/IoClose';
import style from './PanelContent.module.scss';
interface PanelContentProps {
onClose: () => void;
}
export default function PanelContent(props: PropsWithChildren<PanelContentProps>) {
const { onClose, children } = props;
return (
<div className={style.contentWrapper}>
<div className={style.corner}>
<IconButton onClick={onClose} aria-label='close' icon={<IoClose />} variant='ontime-ghosted-white' />
</div>
<div className={style.content}>{children}</div>
</div>
);
}
@@ -0,0 +1,64 @@
@use '../../../theme/ontimeColours' as *;
@use '../../../theme/v2Styles' as *;
.tabs,
ul {
list-style: none;
padding: 0;
margin: 0;
}
.tabs {
width: min(30vw, 300px);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.primary,
.secondary {
padding: 0.25rem 1rem;
margin-right: 1rem;
&:focus {
background-color: $gray-1000;
outline: 0;
}
&:hover {
background-color: $gray-1000;
cursor: pointer;
}
}
.primary {
font-size: 1rem;
border-radius: 2px;
display: flex;
align-items: center;
gap: 0.5rem;
&.active {
color: $blue-400;
background-color: $gray-1100;
}
&.unsaved::before {
content: '';
width: 6px;
height: 6px;
border-radius: 3px;
background-color: $blue-400;
}
&.split {
margin-top: 1rem;
}
}
.secondary {
margin-left: 1rem;
color: $secondary-text-gray;
border-left: 1px solid $white-10;
font-size: $inner-section-text-size;
}
@@ -0,0 +1,55 @@
import { KeyboardEvent } from 'react';
import { cx } from '../../../common/utils/styleUtils';
import { settingPanels, SettingsOption, useSettingsStore } from '../settingsStore';
import style from './PanelList.module.scss';
export default function PanelList() {
const { showSettings, setShowSettings, hasUnsavedChanges } = useSettingsStore();
const handleSelect = (panel: SettingsOption) => {
setShowSettings(panel.id);
};
const isKeyEnter = (event: KeyboardEvent<HTMLLIElement>) => event.key === 'Enter';
return (
<ul className={style.tabs}>
{settingPanels.map((panel) => {
const unsaved = hasUnsavedChanges(panel.id);
const classes = cx([
style.primary,
showSettings === panel.id ? style.active : null,
panel.split ? style.split : null,
unsaved ? style.unsaved : null,
]);
return (
<>
<li
key={panel.id}
onClick={() => handleSelect(panel)}
onKeyDown={(event) => {
isKeyEnter(event) && handleSelect(panel);
}}
className={classes}
tabIndex={0}
role='button'
>
{panel.label}
</li>
{panel.secondary?.map((secondary) => {
return (
<li key={secondary.id} onClick={() => handleSelect(panel)} className={style.secondary}>
{secondary.label}
</li>
);
})}
</>
);
})}
</ul>
);
}
@@ -0,0 +1,37 @@
@use '../../../theme/ontimeColours' as *;
@use '../../../theme/v2Styles' as *;
.header {
font-size: 2rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid $white-10;
font-weight: 600;
}
.subheader {
font-size: 1.25rem;
font-weight: 600;
}
.section {
margin: 2rem 0;
font-size: calc(1rem - 1px);
max-width: 800px;
}
.paragraph {
padding: 0.5rem 0;
}
.card {
padding: 1rem;
background-color: $white-1;
border: 1px solid $gray-1100;
border-radius: 0.25rem;
}
.error {
font-size: $inner-section-text-size;
display: block;
color: $error-red;
}
@@ -0,0 +1,23 @@
import { ReactNode } from 'react';
import style from './Panel.module.scss';
export function Header({ children }: { children: ReactNode }) {
return <h2 className={style.header}>{children}</h2>;
}
export function SubHeader({ children }: { children: ReactNode }) {
return <h3 className={style.subheader}>{children}</h3>;
}
export function Section({ children }: { children: ReactNode }) {
return <p className={style.section}>{children}</p>;
}
export function Paragraph({ children }: { children: ReactNode }) {
return <p className={style.paragraph}>{children}</p>;
}
export function Card({ children }: { children: ReactNode }) {
return <div className={style.card}>{children}</div>;
}
@@ -0,0 +1,35 @@
import { version } from '../../../../../package.json';
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
import { gitbookUrl, githubUrl, websiteUrl } from '../../../../externals';
import * as Panel from '../PanelUtils';
import CheckUpdatesButton from './CheckUpdatesButton';
export default function AboutPanel() {
return (
<>
<Panel.Header>About Ontime</Panel.Header>
<Panel.Section>
<Panel.SubHeader>Ontime</Panel.SubHeader>
<Panel.Paragraph>
Free, open-source software for managing rundowns and event timers
<ExternalLink href={websiteUrl}>www.getontime.no</ExternalLink>
</Panel.Paragraph>
</Panel.Section>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Links</Panel.SubHeader>
<ExternalLink href={gitbookUrl}>Read the docs over at GitBook</ExternalLink>
<ExternalLink href={githubUrl}>Follow the project on GitHub</ExternalLink>
</Panel.Card>
</Panel.Section>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Current version</Panel.SubHeader>
<Panel.Paragraph>{`You are currently using Ontime ${version}`}</Panel.Paragraph>
<CheckUpdatesButton version={version} />
</Panel.Card>
</Panel.Section>
</>
);
}
@@ -0,0 +1,73 @@
import { useState } from 'react';
import { Button } from '@chakra-ui/react';
import { getLatestVersion, HasUpdate } from '../../../../common/api/ontimeApi';
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
import style from '../Panel.module.scss';
type CheckFail = {
error: string;
};
type CheckIsLatest = {
latest: true;
};
type CheckRemote = CheckFail | CheckIsLatest | HasUpdate;
interface CheckUpdatesButtonProps {
version: string;
}
export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
const { version } = props;
const [updateMessage, setUpdateMessage] = useState<CheckRemote | null>(null);
const [isFetching, setIsFetching] = useState(false);
/**
* Handles version comparison and returns component with message
*/
const versionCheck = async () => {
setIsFetching(true);
try {
const latest = await getLatestVersion();
if (!latest.version.includes(version)) {
// new version, pass data to component
setUpdateMessage(latest);
} else {
setUpdateMessage({ latest: true });
}
} catch {
setUpdateMessage({ error: 'Error reaching server' });
} finally {
setIsFetching(false);
}
};
const disableButton = Boolean(updateMessage && 'version' in updateMessage);
return (
<>
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton}>
Check for updates
</Button>
<ResolveUpdateMessage updateMessage={updateMessage} />
</>
);
}
function ResolveUpdateMessage(props: { updateMessage: CheckRemote | null }) {
const { updateMessage } = props;
if (updateMessage && 'error' in updateMessage) {
return <span className={style.error}>{updateMessage.error}</span>;
}
if (updateMessage && 'url' in updateMessage) {
return <ExternalLink href={updateMessage?.url}>{`New version available: ${updateMessage.version}`}</ExternalLink>;
}
return null;
}
@@ -0,0 +1,76 @@
import { create } from 'zustand';
export type SettingsOption = {
id: string;
label: string;
secondary?: SettingsOption[];
split?: boolean;
};
export const settingPanels: SettingsOption[] = [
{ id: 'project', label: 'Project' },
{ id: 'general', label: 'General' },
{ id: 'interface', label: 'Interface' },
{ id: 'views', label: 'Views' },
{
id: 'sources',
label: 'Data Sources',
secondary: [{ id: 'g-sheet', label: 'Sync with Google Sheet' }],
split: true,
},
{
id: 'integrations',
label: 'Integrations',
secondary: [
{ id: 'osc', label: 'OSC Integration' },
{ id: 'http', label: 'HTTP Integration' },
],
},
{ id: 'log', label: 'Log', split: true },
{
id: 'about',
label: 'About',
split: true,
secondary: [
{ id: 'links', label: 'Links' },
{ id: 'version', label: 'Version' },
],
},
] as const;
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
const firstPanel = settingPanels[0].id;
type SettingsStore = {
showSettings: SettingsOptionId | null;
setShowSettings: (panelId?: SettingsOptionId | null) => void;
unsavedChanges: Set<SettingsOptionId>;
hasUnsavedChanges: (panelId: SettingsOptionId) => boolean;
addUnsavedChanges: (panelId: SettingsOptionId) => void;
removeUnsavedChanges: (panelId: SettingsOptionId) => void;
};
export const useSettingsStore = create<SettingsStore>((set, get) => ({
showSettings: null,
setShowSettings: (panelId?: SettingsOptionId | null) => {
const newPanel = panelId === undefined ? firstPanel : panelId;
set((state) => {
return {
...state,
showSettings: newPanel,
};
});
},
unsavedChanges: new Set(),
hasUnsavedChanges: (panelId: SettingsOptionId) => get().unsavedChanges.has(panelId),
addUnsavedChanges: (panelId: SettingsOptionId) =>
set((state) => {
state.unsavedChanges.add(panelId);
return { unsavedChanges: new Set(state.unsavedChanges) };
}),
removeUnsavedChanges: (panelId: SettingsOptionId) =>
set((state) => {
state.unsavedChanges.delete(panelId);
return { unsavedChanges: new Set(state.unsavedChanges) };
}),
}));
@@ -13,6 +13,7 @@ $playback-width: 26rem;
color: $ui-white;
transition-property: color;
transition-duration: $transition-time-action;
&:hover {
color: $ontime-color;
}
@@ -28,152 +29,54 @@ $playback-width: 26rem;
background: $ui-black;
width: 100%;
height: 100%;
margin: auto;
color: $ui-white;
padding: 1rem 0.5rem;
font-family: $ontime-font-family;
display: grid;
grid-template-columns: $menu-width auto;
grid-template-rows: 2rem 1fr;
grid-template-areas:
'menu overview'
'menu main';
gap: 0.5rem;
}
.overview {
grid-area: overview;
background-color: $white-10;
}
.panelContainer {
grid-area: main;
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: $menu-width $rundown-width $playback-width auto;
grid-template-columns: $rundown-width $playback-width 1fr;
grid-template-areas:
'sett rundown play info'
'sett rundown mess info';
'rundown play info'
'rundown mess info';
gap: 0.5rem;
overflow: hidden;
.rundown,
.playback,
.messages,
.info,
.settings {
.info {
position: relative;
border-radius: 8px;
height: 100%;
background-color: $bg-container-l2;
padding: 1rem;
.corner {
/* we show this if the component hasnt been extracted */
display: inline;
}
}
}
/* 2/3 window, hide info */
@media (max-width: 1450px) and (min-height: 700px) {
.mainContainer {
height: 100%;
grid-template-rows: auto 1fr;
grid-template-columns: $menu-width 1fr $playback-width;
.info {
visibility: hidden;
}
}
}
/* 1/2 window, event list only */
@media (max-width: 1100px) {
.mainContainer {
height: 100%;
grid-template-rows: 100%;
grid-template-columns: $menu-width $rundown-width;
grid-template-areas:
'sett rundown';
.info,
.messages,
.playback {
visibility: hidden;
}
}
}
/* 1/3 window, show control only */
@media (max-width: 850px) and (min-height: 500px) {
.mainContainer {
grid-template-rows: auto 1fr;
grid-template-columns: 100%;
grid-template-areas:
'play'
'mess';
.playback,
.messages {
visibility: visible;
}
.rundown,
.info,
.settings {
visibility: hidden;
}
}
}
/* 1/3 corner window, playback only */
@media (max-width: 850px) and (max-height: 500px) {
.mainContainer {
grid-template-rows: 100%;
grid-template-columns: 100%;
grid-template-areas: 'play';
.playback {
visibility: visible;
}
.rundown,
.messages,
.info,
.settings {
visibility: hidden;
}
}
}
.mainContainer {
.settings,
.rundown,
.messages,
.playback,
.info {
border-radius: 8px;
height: 100%;
background-color: $bg-container-l2;
padding: 1rem;
display: flex;
flex-direction: column;
}
}
.eventEditor {
border-radius: 8px 8px 0 0;
background-color: $bg-container-l2;
box-shadow: $large-bottom-drawer-shadow;
border-top: 1px solid $white-20;
position: absolute;
bottom: 0;
width: 100vw;
left: 0;
z-index: 10;
color: white;
transition: bottom $transition-time-feedback;
&.noEvent {
bottom: -500px;
transition: bottom 0.7s;
}
.eventEditorLayout {
display: flex;
}
.header {
background-color: $gray-1250;
padding: 0.5rem;
border-left: 1px solid $white-10;
border-radius: 0 8px 0 0;
}
}
.rundown {
grid-area: rundown;
height: 100%;
.content {
height: calc(100% - 1.5rem);
@@ -204,22 +107,6 @@ $playback-width: 26rem;
min-width: 26rem;
}
.mainContainer > .settings {
grid-area: sett;
background-color: transparent;
margin: 0;
padding: 0 0.5rem 0 0;
width: fit-content;
display: flex;
flex-direction: column;
}
.mainContainer > .rundown {
padding: 1rem 0;
}
.content {
padding-top: 1.5rem;
}
+62 -23
View File
@@ -2,6 +2,8 @@ import { lazy, useEffect } from 'react';
import { useDisclosure } from '@chakra-ui/react';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import AppSettings from '../app-settings/AppSettings';
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
import MenuBar from '../menu/MenuBar';
import AboutModal from '../modals/about-modal/AboutModal';
import QuickStart from '../modals/quick-start/QuickStart';
@@ -18,8 +20,31 @@ const EventEditor = lazy(() => import('../event-editor/EventEditorExport'));
const IntegrationModal = lazy(() => import('../modals/integration-modal/IntegrationModal'));
const SettingsModal = lazy(() => import('../modals/settings-modal/SettingsModal'));
// TODO: add breakpoints for body font size ??
// - 15px for normal
// - 16px for large screens
// TODO: can we delete all the font-family stuff and leave it only at the top?
// TODO: change scrollbar colours to use ontime stuff?
// TODO: rename v2Styles to appStyles?
// TODO: remove onAir as a setting
// TODO: add error boundaries
// TODO: should nav menu have same rules as app settings
export default function Editor() {
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
const showSettings = useSettingsStore((state) => state.showSettings);
const setShowSettings = useSettingsStore((state) => state.setShowSettings);
const handleSettings = (newTab?: SettingsOptionId) => {
setShowSettings(newTab);
};
const { isOpen: isOldSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
const {
isOpen: isIntegrationModalOpen,
@@ -34,6 +59,8 @@ export default function Editor() {
document.title = 'ontime - Editor';
}, []);
const isSettingsOpen = Boolean(showSettings);
return (
<>
<ErrorBoundary>
@@ -41,30 +68,42 @@ export default function Editor() {
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
<SettingsModal isOpen={isSettingsOpen} onClose={onSettingsClose} />
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
</ErrorBoundary>
<div className={styles.mainContainer} data-testid='event-editor'>
<div id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar
onSettingsOpen={onSettingsOpen}
isSettingsOpen={isSettingsOpen}
onSettingsClose={onSettingsClose}
isUploadOpen={isUploadModalOpen}
onUploadOpen={onUploadModalOpen}
isIntegrationOpen={isIntegrationModalOpen}
onIntegrationOpen={onIntegrationModalOpen}
isAboutOpen={isAboutModalOpen}
onAboutOpen={onAboutModalOpen}
isQuickStartOpen={isQuickStartOpen}
onQuickStartOpen={onQuickStartOpen}
/>
</ErrorBoundary>
<div className={styles.mainContainer} data-testid='editor-container'>
<ErrorBoundary>
<MenuBar
isOldSettingsOpen={isOldSettingsOpen}
onSettingsOpen={onSettingsOpen}
onSettingsClose={onSettingsClose}
isUploadOpen={isUploadModalOpen}
onUploadOpen={onUploadModalOpen}
isIntegrationOpen={isIntegrationModalOpen}
onIntegrationOpen={onIntegrationModalOpen}
isAboutOpen={isAboutModalOpen}
onAboutOpen={onAboutModalOpen}
isQuickStartOpen={isQuickStartOpen}
onQuickStartOpen={onQuickStartOpen}
openSettings={handleSettings}
isSettingsOpen={isSettingsOpen}
/>
</ErrorBoundary>
{showSettings ? (
<AppSettings />
) : (
<div id='panels' className={styles.panelContainer}>
<Rundown />
<MessageControl />
<TimerControl />
<Info />
</div>
)}
<div className={styles.overview}>
<ErrorBoundary></ErrorBoundary>
{
// TODO: the information about the event
}
</div>
<Rundown />
<MessageControl />
<TimerControl />
<Info />
</div>
<EventEditor />
</>
@@ -1,5 +1,36 @@
@use '../../theme/ontimeColours' as *;
@use '../../theme/v2Styles' as *;
.eventEditorContainer {
border-radius: 8px 8px 0 0;
background-color: $bg-container-l2;
box-shadow: $large-bottom-drawer-shadow;
border-top: 1px solid $white-20;
position: absolute;
bottom: 0;
width: 100vw;
left: 0;
z-index: 10;
color: white;
transition: bottom $transition-time-feedback;
&.noEvent {
bottom: -500px;
transition: bottom 0.7s;
}
.eventEditorLayout {
display: flex;
}
.header {
background-color: $gray-1250;
padding: 0.5rem;
border-left: 1px solid $white-10;
border-radius: 0 8px 0 0;
}
}
.eventEditor {
padding: 1rem 2rem 2rem 2rem;
width: 100%;
@@ -8,21 +8,13 @@ import { cx } from '../../common/utils/styleUtils';
import EventEditor from './EventEditor';
import style from '../editors/Editor.module.scss';
/* Styling for action buttons */
const closeBtnStyle = {
size: 'md',
variant: 'ghost',
colorScheme: 'white',
_hover: { bg: '#ebedf0', color: '#333' },
};
import style from './EventEditor.module.scss';
const EventEditorExport = () => {
const editId = useAppMode((state) => state.editId);
const setEditId = useAppMode((state) => state.setEditId);
const editorStyle = cx([style.eventEditor, !editId ? style.noEvent : null]);
const editorStyle = cx([style.eventEditorContainer, !editId ? style.noEvent : null]);
const removeOpenEvent = () => setEditId(null);
return (
@@ -31,7 +23,12 @@ const EventEditorExport = () => {
<div className={style.eventEditorLayout}>
<EventEditor />
<div className={style.header}>
<IconButton aria-label='Close Menu' icon={<IoClose />} onClick={removeOpenEvent} {...closeBtnStyle} />
<IconButton
aria-label='Close Menu'
icon={<IoClose />}
onClick={removeOpenEvent}
variant='ontime-ghosted-white'
/>
</div>
</div>
</ErrorBoundary>
@@ -1,9 +1,17 @@
@use '../../theme/ontimeColours' as *;
.menu {
grid-area: menu;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5em;
}
.gap {
height: 1em;
}
.open {
background: $blue-700;
}
}
+19 -6
View File
@@ -1,5 +1,4 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { VStack } from '@chakra-ui/react';
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
@@ -20,7 +19,7 @@ import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
import style from './MenuBar.module.scss';
interface MenuBarProps {
isSettingsOpen: boolean;
isOldSettingsOpen: boolean;
onSettingsOpen: () => void;
onSettingsClose: () => void;
isUploadOpen: boolean;
@@ -31,6 +30,8 @@ interface MenuBarProps {
onAboutOpen: () => void;
isQuickStartOpen: boolean;
onQuickStartOpen: () => void;
openSettings: (newTab?: string) => void;
isSettingsOpen: boolean;
}
const buttonStyle = {
@@ -47,7 +48,7 @@ const buttonStyle = {
const MenuBar = (props: MenuBarProps) => {
const {
isSettingsOpen,
isOldSettingsOpen,
onSettingsOpen,
onSettingsClose,
isUploadOpen,
@@ -58,6 +59,8 @@ const MenuBar = (props: MenuBarProps) => {
onAboutOpen,
isQuickStartOpen,
onQuickStartOpen,
openSettings,
isSettingsOpen,
} = props;
const { isElectron, sendToElectron } = useElectronEvent();
@@ -118,7 +121,7 @@ const MenuBar = (props: MenuBarProps) => {
};
return (
<VStack>
<div className={style.menu}>
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} size='md' />
<div className={style.gap} />
@@ -188,7 +191,7 @@ const MenuBar = (props: MenuBarProps) => {
{...buttonStyle}
isDisabled={appMode === AppMode.Run}
icon={<IoSettingsOutline />}
className={isSettingsOpen ? style.open : ''}
className={isOldSettingsOpen ? style.open : ''}
clickHandler={onSettingsOpen}
tooltip='Settings'
aria-label='Settings'
@@ -204,7 +207,17 @@ const MenuBar = (props: MenuBarProps) => {
aria-label='About'
size='sm'
/>
</VStack>
<div className={style.gap} />
<TooltipActionBtn
{...buttonStyle}
className={isSettingsOpen ? style.open : ''}
icon={<IoSettingsOutline />}
clickHandler={() => openSettings()}
tooltip='About'
aria-label='About'
size='sm'
/>
</div>
);
};
+8
View File
@@ -47,6 +47,14 @@ export const ontimeButtonSubtle = {
},
};
// TODO: revise colours
export const ontimeButtonGhostedWhite = {
...ontimeButtonSubtle,
backgroundColor: 'transparent',
color: 'white',
_hover: { background: '#ebedf0', color: '#333' },
};
export const ontimeButtonGhosted = {
...ontimeButtonSubtle,
backgroundColor: 'transparent',
+2
View File
@@ -4,6 +4,7 @@ import { ontimeAlertOnLight } from './OntimeAlert';
import {
ontimeButtonFilled,
ontimeButtonGhosted,
ontimeButtonGhostedWhite,
ontimeButtonOutlined,
ontimeButtonSubtle,
ontimeButtonSubtleOnLight,
@@ -46,6 +47,7 @@ const theme = extendTheme({
'ontime-outlined': { ...ontimeButtonOutlined },
'ontime-subtle': { ...ontimeButtonSubtle },
'ontime-ghosted': { ...ontimeButtonGhosted },
'ontime-ghosted-white': { ...ontimeButtonGhostedWhite },
'ontime-subtle-white': { ...ontimeButtonSubtleWhite },
'ontime-subtle-on-light': { ...ontimeButtonSubtleOnLight },
'ontime-ghost-on-light': { ...ontimeGhostOnLight },
+1 -1
View File
@@ -413,7 +413,7 @@
},
"settings": {
"app": "ontime",
"version": "2.0.0",
"version": "3.0.0-alpha",
"serverPort": 4001,
"editorKey": null,
"operatorKey": null,
+1 -1
View File
@@ -8,7 +8,7 @@ test.describe('pages routes are available', () => {
await page.goto('http://localhost:4001/editor');
await expect(page).toHaveTitle(/ontime/);
await expect(page.getByTestId('event-editor')).toBeVisible();
await expect(page.getByTestId('editor-container')).toBeVisible();
await expect(page.getByTestId('panel-rundown')).toBeVisible();
await expect(page.getByTestId('panel-timer-control')).toBeVisible();
await expect(page.getByTestId('panel-messages-control')).toBeVisible();