diff --git a/apps/client/src/common/components/state/Empty.module.scss b/apps/client/src/common/components/state/Empty.module.scss index 345b3057f..59b67c7f9 100644 --- a/apps/client/src/common/components/state/Empty.module.scss +++ b/apps/client/src/common/components/state/Empty.module.scss @@ -1,20 +1,43 @@ +@use '@/theme/viewerDefs' as *; + .emptyContainer { width: 100%; text-align: center; - color: $white-10; + color: var(--secondary-color-override, $viewer-secondary-color); .empty { display: block; - width: min(100%, 24rem); - margin-inline: auto; - opacity: 0.8; + width: min(100%, 14rem); + margin: 0 auto -1.5rem; + opacity: 0.6; } .text { display: block; margin-inline: auto; - font-weight: 600; - font-size: 2em; - max-width: min(100%, 600px); + font-weight: 400; + font-size: clamp(1rem, 1.55vw, 1.5rem); + line-height: 1.35; + max-width: min(100%, 40rem); + } + + &.error { + color: $error-red; + + .empty { + opacity: 0.35; + filter: grayscale(1); + } + + .text { + margin-top: 0.5rem; + } + } + + .errorIcon { + display: block; + width: 1.5rem; + height: 1.5rem; + margin: -0.125rem auto 0; } } diff --git a/apps/client/src/common/components/state/Empty.tsx b/apps/client/src/common/components/state/Empty.tsx index 0e88e2868..d5bad4436 100644 --- a/apps/client/src/common/components/state/Empty.tsx +++ b/apps/client/src/common/components/state/Empty.tsx @@ -1,4 +1,5 @@ import { CSSProperties } from 'react'; +import { IoWarningOutline } from 'react-icons/io5'; import EmptyImage from '../../../assets/images/empty.svg?react'; import { cx } from '../../utils/styleUtils'; @@ -9,12 +10,18 @@ interface EmptyProps { text?: string; injectedStyles?: CSSProperties; className?: string; + variant?: 'error'; } -export default function Empty({ text, className, injectedStyles }: EmptyProps) { +export default function Empty({ text, className, injectedStyles, variant }: EmptyProps) { return ( -
+
+ {variant === 'error' && } {text && {text}}
); diff --git a/apps/client/src/common/components/state/EmptyFill.module.scss b/apps/client/src/common/components/state/EmptyFill.module.scss new file mode 100644 index 000000000..368d726dd --- /dev/null +++ b/apps/client/src/common/components/state/EmptyFill.module.scss @@ -0,0 +1,11 @@ +.fill { + box-sizing: border-box; + width: 100%; + height: 100%; + min-height: 0; + flex: 1; + + display: grid; + place-items: start center; + padding: clamp(4rem, 20dvh, 12rem) 1.5rem 1rem; +} diff --git a/apps/client/src/common/components/state/EmptyFill.tsx b/apps/client/src/common/components/state/EmptyFill.tsx new file mode 100644 index 000000000..a676e8469 --- /dev/null +++ b/apps/client/src/common/components/state/EmptyFill.tsx @@ -0,0 +1,19 @@ +import { cx } from '../../utils/styleUtils'; +import Empty from './Empty'; + +import style from './EmptyFill.module.scss'; + +interface EmptyFillProps { + text?: string; + /** placed on the fill wrapper — e.g. to assign a grid-area in a grid parent */ + className?: string; +} + +/** Container-filling empty/loading state for panels and grid/flex cells. */ +export default function EmptyFill({ text, className }: EmptyFillProps) { + return ( +
+ +
+ ); +} diff --git a/apps/client/src/common/components/state/EmptyPage.module.scss b/apps/client/src/common/components/state/EmptyPage.module.scss index 0f09063e7..d77f39f3a 100644 --- a/apps/client/src/common/components/state/EmptyPage.module.scss +++ b/apps/client/src/common/components/state/EmptyPage.module.scss @@ -6,7 +6,7 @@ box-sizing: border-box; /* reset */ overflow: hidden; width: 100%; /* restrict the page width to viewport */ - height: 100vh; + height: 100dvh; font-family: var(--font-family-override, $viewer-font-family); background: var(--background-color-override, $viewer-background-color); @@ -16,5 +16,6 @@ display: flex; flex-direction: column; align-items: center; - padding-top: 5rem; + justify-content: center; + padding-block: 5rem; } diff --git a/apps/client/src/common/components/state/EmptyPage.tsx b/apps/client/src/common/components/state/EmptyPage.tsx index b589989e5..7fb843093 100644 --- a/apps/client/src/common/components/state/EmptyPage.tsx +++ b/apps/client/src/common/components/state/EmptyPage.tsx @@ -7,12 +7,13 @@ import style from './EmptyPage.module.scss'; interface EmptyPageProps { text?: string; injectedStyles?: CSSProperties; + variant?: 'error'; } -export default function EmptyPage({ text, injectedStyles }: EmptyPageProps) { +export default function EmptyPage({ text, injectedStyles, variant }: EmptyPageProps) { return (
- +
); } diff --git a/apps/client/src/common/components/state/EmptyTableBody.module.scss b/apps/client/src/common/components/state/EmptyTableBody.module.scss index b2288ffef..969ed4b48 100644 --- a/apps/client/src/common/components/state/EmptyTableBody.module.scss +++ b/apps/client/src/common/components/state/EmptyTableBody.module.scss @@ -15,9 +15,4 @@ gap: 1rem; margin-top: 1em; } - - .text { - font-weight: 600; - font-size: 2em; - } } diff --git a/apps/client/src/common/components/state/EmptyTableBody.tsx b/apps/client/src/common/components/state/EmptyTableBody.tsx index 0edd2a22c..94cb7aa9b 100644 --- a/apps/client/src/common/components/state/EmptyTableBody.tsx +++ b/apps/client/src/common/components/state/EmptyTableBody.tsx @@ -18,8 +18,7 @@ export default function EmptyTableBody({ handleAddNew }: EmptyTableBodyProps) { - - {text} + {handleAddNew && (
); diff --git a/apps/client/src/features/rundown/RundownList.tsx b/apps/client/src/features/rundown/RundownList.tsx index 5718a8ea1..86d80906b 100644 --- a/apps/client/src/features/rundown/RundownList.tsx +++ b/apps/client/src/features/rundown/RundownList.tsx @@ -1,19 +1,24 @@ import { memo } from 'react'; -import Empty from '../../common/components/state/Empty'; +import EmptyFill from '../../common/components/state/EmptyFill'; import { useRundownWithMetadata } from '../../common/hooks-query/useRundown'; import { useRundownEditor } from '../../common/hooks/useSocket'; +import { useTranslation } from '../../translation/TranslationProvider'; import Rundown from './Rundown'; export default memo(RundownList); function RundownList() { const { data, status, rundownMetadata } = useRundownWithMetadata(); const featureData = useRundownEditor(); + const { getLocalizedString } = useTranslation(); - const isLoading = status !== 'success' || !data || !rundownMetadata; + // avoid showing the editable empty state before we know whether the rundown is actually empty + if (status === 'pending') { + return ; + } - if (isLoading) { - return ; + if (status === 'error') { + return ; } return ( diff --git a/apps/client/src/features/rundown/rundown-table/RundownTable.tsx b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx index b7a37bd0f..03f28a277 100644 --- a/apps/client/src/features/rundown/rundown-table/RundownTable.tsx +++ b/apps/client/src/features/rundown/rundown-table/RundownTable.tsx @@ -1,6 +1,5 @@ import { memo, useEffect, useMemo } from 'react'; -import EmptyPage from '../../../common/components/state/EmptyPage'; import { EntryActionsProvider } from '../../../common/context/EntryActionsContext'; import useCustomFields from '../../../common/hooks-query/useCustomFields'; import { useLoadedRundownSource } from '../../../common/hooks-query/useScopedRundown'; @@ -13,7 +12,7 @@ import { makeRundownColumns } from './makeRundownColumns'; export default memo(RundownTable); function RundownTable() { - const { data: customFields, status: customFieldStatus } = useCustomFields(); + const { data: customFields } = useCustomFields(); const setPermissions = useCuesheetPermissions((state) => state.setPermissions); const { editorMode } = useEditorFollowMode(); const source = useLoadedRundownSource(); @@ -32,16 +31,10 @@ function RundownTable() { const columns = useMemo(() => makeRundownColumns(customFields), [customFields]); - const isLoading = !customFields || customFieldStatus === 'pending'; - return ( - {isLoading ? ( - - ) : ( - - )} + ); diff --git a/apps/client/src/views/backstage/Backstage.tsx b/apps/client/src/views/backstage/Backstage.tsx index 229ba5213..85e18bf7c 100644 --- a/apps/client/src/views/backstage/Backstage.tsx +++ b/apps/client/src/views/backstage/Backstage.tsx @@ -35,7 +35,7 @@ export default function BackstageLoader() { } if (status === 'error') { - return ; + return ; } return ; diff --git a/apps/client/src/views/common/loader/Loader.module.scss b/apps/client/src/views/common/loader/Loader.module.scss index 0d00acd37..62a937a14 100644 --- a/apps/client/src/views/common/loader/Loader.module.scss +++ b/apps/client/src/views/common/loader/Loader.module.scss @@ -7,7 +7,7 @@ $dot-spacing: 1.5rem; display: grid; place-items: center; background-color: var(--background-color-override, $viewer-background-color); - height: 100vh; + height: 100dvh; } .ellipsis { @@ -21,26 +21,26 @@ $dot-spacing: 1.5rem; height: $dot-size; border-radius: 50%; background-color: var(--accent-color-override, $ontime-color); - animation-timing-function: cubic-bezier(0, 1, 1, 0); + animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); &:nth-child(1) { left: $dot-size; - animation: lds-ellipsis1 0.6s infinite; + animation: lds-ellipsis1 1s infinite; } &:nth-child(2) { left: $dot-size; - animation: lds-ellipsis2 0.6s infinite; + animation: lds-ellipsis2 1s infinite; } &:nth-child(3) { left: calc($dot-size + $dot-spacing); - animation: lds-ellipsis2 0.6s infinite; + animation: lds-ellipsis2 1s infinite; } &:nth-child(4) { left: calc($dot-size + 2 * $dot-spacing); - animation: lds-ellipsis3 0.6s infinite; + animation: lds-ellipsis3 1s infinite; } } } diff --git a/apps/client/src/views/countdown/Countdown.scss b/apps/client/src/views/countdown/Countdown.scss index a5a175c8f..f36f03bca 100644 --- a/apps/client/src/views/countdown/Countdown.scss +++ b/apps/client/src/views/countdown/Countdown.scss @@ -80,21 +80,7 @@ $item-height: 3.5rem; display: flex; flex-direction: column; align-items: center; - - button { - margin-top: 1.75rem; - } - } - - .empty-state__content { - max-width: none; - - span { - max-width: none; - white-space: nowrap; - font-size: clamp(1.5rem, 4vw, 2.25rem); - line-height: 1.1; - } + gap: 1.5rem; } .list-container { diff --git a/apps/client/src/views/countdown/Countdown.tsx b/apps/client/src/views/countdown/Countdown.tsx index 40db224db..9db4cce6e 100644 --- a/apps/client/src/views/countdown/Countdown.tsx +++ b/apps/client/src/views/countdown/Countdown.tsx @@ -43,7 +43,7 @@ export default function CountdownLoader() { } if (status === 'error') { - return ; + return ; } return ; @@ -87,7 +87,7 @@ function Countdown({ customFields, rundownData, projectData, isMirrored, setting {!hasEvents && (
- +
)} @@ -121,7 +121,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod if (subscriptions.length === 0) { return (
- + @@ -137,7 +137,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod if (subscribedEvents.length === 0) { return (
- + @@ -154,7 +154,7 @@ function CountdownContents({ candidates, rundownData, subscriptions, goToEditMod if (eventsToShow.length === 0) { return (
- +
); } diff --git a/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx index 595e221ba..048f8f1c6 100644 --- a/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx +++ b/apps/client/src/views/cuesheet/CuesheetTableWrapper.tsx @@ -2,7 +2,6 @@ import { MaybeString, ProjectRundown } from 'ontime-types'; import { memo, use, useMemo } from 'react'; import Select from '../../common/components/select/Select'; -import EmptyPage from '../../common/components/state/EmptyPage'; import { PresetContext } from '../../common/context/PresetContext'; import useCustomFields from '../../common/hooks-query/useCustomFields'; import type { RundownSource } from '../../common/hooks-query/useScopedRundown'; @@ -34,40 +33,34 @@ function CuesheetTableWrapper({ const preset = use(PresetContext); const isCurrentRundown = source.rundownId !== null && source.rundownId === loadedRundownId; const { cuesheetMode, setCuesheetMode } = useApplyCuesheetPolicy(preset, { canRunMode: isCurrentRundown }); - const { data: customFields, status: customFieldStatus } = useCustomFields(); + const { data: customFields } = useCustomFields(); const columns = useMemo( () => makeCuesheetColumns(customFields, cuesheetMode, preset), [customFields, cuesheetMode, preset], ); - const isLoading = !customFields || customFieldStatus === 'pending'; - return ( - {isLoading ? ( - - ) : ( - - - - } - /> - )} + + + + } + /> ); } diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss index f95eb6468..b8873cca1 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.module.scss @@ -5,6 +5,10 @@ $table-header-font-size: calc(1rem - 2px); @include rows.cuesheet-row-columns($table-header-font-size); +.tableLoading { + grid-area: table; +} + .cuesheet { font-size: $table-font-size; font-weight: 400; diff --git a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx index 32c3f60ac..075f0b898 100644 --- a/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx +++ b/apps/client/src/views/cuesheet/cuesheet-table/CuesheetTable.tsx @@ -11,7 +11,7 @@ import { TableVirtuosoHandle, } from 'react-virtuoso'; -import EmptyPage from '../../../common/components/state/EmptyPage'; +import EmptyFill from '../../../common/components/state/EmptyFill'; import EmptyTableBody from '../../../common/components/state/EmptyTableBody'; import { useEntryActionsContext } from '../../../common/context/EntryActionsContext'; import type { RundownSource } from '../../../common/hooks-query/useScopedRundown'; @@ -19,6 +19,7 @@ import type { ExtendedEntry } from '../../../common/utils/rundownMetadata'; import { usePersistedRundownOptions } from '../../../features/rundown/rundown.options'; import { useEventSelection } from '../../../features/rundown/useEventSelection'; import { AppMode } from '../../../ontimeConfig'; +import { useTranslation } from '../../../translation/TranslationProvider'; import { usePersistedCuesheetOptions } from '../cuesheet.options'; import { useCuesheetPermissions } from '../useTablePermissions'; import { CuesheetHeader, SortableCuesheetHeader } from './cuesheet-table-elements/CuesheetHeader'; @@ -64,6 +65,7 @@ export default function CuesheetTable({ }: CuesheetTableProps) { const { flatRundown, status, selectedEventId } = source; const { updateEntry, updateTimer, addEntry } = useEntryActionsContext(); + const { getLocalizedString } = useTranslation(); const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit; const useOptions = tableRoot === 'editor' ? usePersistedRundownOptions : usePersistedCuesheetOptions; @@ -228,10 +230,13 @@ export default function CuesheetTable({ }); }, [cuesheetMode, hideIndexColumn, table]); - const isLoading = !flatRundown || status === 'pending'; + // avoid showing the editable empty state before we know whether the rundown is actually empty + if (status === 'pending') { + return ; + } - if (isLoading) { - return ; + if (status === 'error') { + return ; } return ( diff --git a/apps/client/src/views/project-info/ProjectInfo.scss b/apps/client/src/views/project-info/ProjectInfo.scss index 772757521..6901a36be 100644 --- a/apps/client/src/views/project-info/ProjectInfo.scss +++ b/apps/client/src/views/project-info/ProjectInfo.scss @@ -100,13 +100,17 @@ $content-width: min(100%, 1100px); } .info__image-container { - flex: 0 0 min(128px, 25%); + display: flex; + justify-content: center; + align-items: center; + flex: 0 0 min(192px, 25%); } .info__image { display: block; - width: 100%; + max-width: 100%; height: auto; + object-fit: contain; } .link.info__value { diff --git a/apps/client/src/views/project-info/ProjectInfo.tsx b/apps/client/src/views/project-info/ProjectInfo.tsx index 8e6c1f634..c14be9254 100644 --- a/apps/client/src/views/project-info/ProjectInfo.tsx +++ b/apps/client/src/views/project-info/ProjectInfo.tsx @@ -22,7 +22,7 @@ export default function ProjectInfoLoader() { } if (status === 'error') { - return ; + return ; } return ; @@ -73,12 +73,11 @@ function ProjectInfo({ projectData, isMirrored }: ProjectInfoData) {
)} {projectData.custom.map((info, idx) => { - const hasImage = Boolean(info.url); return ( // oxlint-disable-next-line react/no-array-index-key - we only have the index to go of here
{info.title &&
{info.title}
} - {hasImage ? ( + {info.url ? (
{info.value &&
{info.value}
} @@ -108,10 +107,6 @@ function InfoCard({ label, children }: InfoCardProps) { ); } -/** - * Renders an user provided image, collapsing itself if the image fails to load - * We remove the container along with the image to avoid leaving an empty gap in the card - */ function InfoImage({ src }: { src: string }) { const [hasError, setHasError] = useState(false); diff --git a/apps/client/src/views/studio/Studio.tsx b/apps/client/src/views/studio/Studio.tsx index e363dc7b8..53fd4014e 100644 --- a/apps/client/src/views/studio/Studio.tsx +++ b/apps/client/src/views/studio/Studio.tsx @@ -25,7 +25,7 @@ export default function StudioLoader() { } if (status === 'error') { - return ; + return ; } return ; diff --git a/apps/client/src/views/timeline/TimelinePage.tsx b/apps/client/src/views/timeline/TimelinePage.tsx index f505bf06a..cff50cd06 100644 --- a/apps/client/src/views/timeline/TimelinePage.tsx +++ b/apps/client/src/views/timeline/TimelinePage.tsx @@ -1,6 +1,7 @@ import { OntimeView } from 'ontime-types'; import { useMemo } from 'react'; +import EmptyFill from '../../common/components/state/EmptyFill'; import EmptyPage from '../../common/components/state/EmptyPage'; import ViewLogo from '../../common/components/view-logo/ViewLogo'; import ViewParamsEditor from '../../common/components/view-params-editor/ViewParamsEditor'; @@ -29,7 +30,7 @@ export default function TimelinePageLoader() { } if (status === 'error') { - return ; + return ; } return ; @@ -73,7 +74,7 @@ function TimelinePage({ events, customFields, projectData, settings }: TimelineD totalDuration={totalDuration} /> ) : ( - + )}
); diff --git a/apps/client/src/views/timer/Timer.tsx b/apps/client/src/views/timer/Timer.tsx index 3ae139d26..f9708360b 100644 --- a/apps/client/src/views/timer/Timer.tsx +++ b/apps/client/src/views/timer/Timer.tsx @@ -43,7 +43,7 @@ export default function TimerLoader() { } if (status === 'error') { - return ; + return ; } return ;