diff --git a/webui/src/app/_layout.tsx b/webui/src/app/_layout.tsx index 3e52b8a..1f98f7a 100644 --- a/webui/src/app/_layout.tsx +++ b/webui/src/app/_layout.tsx @@ -1,10 +1,9 @@ import { Slot } from 'expo-router'; -import { useColorScheme } from 'nativewind'; -import { useEffect } from 'react'; import { ModuleFilterProvider } from '~/providers/modules'; import { QueryProvider } from '~/providers/query'; import { StatsEntryProvider } from '~/providers/stats'; +import { ThemeProvider } from '~/providers/theme'; // Import the Expo-required radix styles import '@radix-ui/colors/green.css'; @@ -31,28 +30,15 @@ import '~/styles-expo.css'; import '~/styles.css'; export default function RootLayout() { - useWorkaroundForThemeClass(); - return ( - - - - - + + + + + + + ); } - -function useWorkaroundForThemeClass() { - const { colorScheme } = useColorScheme(); - - useEffect(() => { - if (document.body) { - document.body.classList.remove('light-theme', 'dark-theme'); - if (colorScheme) { - document.body.className = `${colorScheme}-theme`; - } - } - }, [colorScheme]); -} diff --git a/webui/src/components/forms/StatsEntrySelect.tsx b/webui/src/components/forms/StatsEntrySelect.tsx index dcf8de0..01fb94a 100644 --- a/webui/src/components/forms/StatsEntrySelect.tsx +++ b/webui/src/components/forms/StatsEntrySelect.tsx @@ -32,9 +32,9 @@ export function StatsEntrySelect() { side="bottom" collisionPadding={{ left: 16, right: 16 }} className={cn( - 'flex min-w-[220px] flex-col gap-0.5 rounded-md border border-default bg-default p-1 shadow-md' - // 'will-change-[opacity,transform]', - // 'data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade' + 'flex min-w-[220px] flex-col gap-0.5 rounded-md border border-default bg-default p-1 shadow-md', + 'transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:duration-300', + 'data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-1/3 data-[state=open]:fade-in data-[state=open]:slide-in-from-top-1/3' )} > diff --git a/webui/src/components/graphs/Graph.tsx b/webui/src/components/graphs/Graph.tsx index 0fb8a27..018f479 100644 --- a/webui/src/components/graphs/Graph.tsx +++ b/webui/src/components/graphs/Graph.tsx @@ -24,9 +24,18 @@ export const Graph = forwardRef(ref: RefObject, initialHeight = 300) { +let lastKnownHeight = 300; + +function useDynamicHeight( + ref: RefObject, + initialHeight = lastKnownHeight +) { const [size, setSize] = useState({ height: initialHeight, width: 0 }); + useEffect(() => { + lastKnownHeight = size.height; + }, [size.height]); + useEffect(() => { const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { diff --git a/webui/src/providers/stats.tsx b/webui/src/providers/stats.tsx index 921e345..e6d07c0 100644 --- a/webui/src/providers/stats.tsx +++ b/webui/src/providers/stats.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { type PropsWithChildren, createContext, useContext, useMemo, useState } from 'react'; +import { Spinner } from '~/ui/Spinner'; import { fetchApi } from '~/utils/api'; import { type PartialStatsEntry } from '~core/data/types'; @@ -24,18 +25,51 @@ export const useStatsEntryContext = () => useContext(statsEntryContext); export function StatsEntryProvider({ children }: PropsWithChildren) { const entries = useStatsEntriesData(); - const [entryId, setEntryId] = useState('2'); + const [entryId, setEntryId] = useState(); + const entryIdOrFirstEntry = entryId ?? entries.data?.[0]?.id; + const entry = useMemo( - () => entries.data?.find((entry) => entry.id === entryId), - [entries, entryId] + () => entries.data?.find((entry) => entry.id === entryIdOrFirstEntry), + [entries, entryIdOrFirstEntry] ); function entryFilePath(absolutePath: string) { return entry?.projectRoot ? absolutePath.replace(entry.projectRoot + '/', '') : absolutePath; } + // TODO: add better UX for loading + if (entries.isFetching && !entries.data?.length) { + return ( + + + + ); + } + + // TODO: add better UX for empty state + if (entries.isFetched && !entries.data?.length) { + return ( + + No stats found. + Open your app in the browser, or device, to collect the stats. + + ); + } + + // TODO: add better UX for error state + if (!entryIdOrFirstEntry) { + return ( + + Unable to load stats. + Make sure you configured Expo Atlas properly. + + ); + } + return ( - + {children} ); diff --git a/webui/src/providers/theme.tsx b/webui/src/providers/theme.tsx new file mode 100644 index 0000000..20570af --- /dev/null +++ b/webui/src/providers/theme.tsx @@ -0,0 +1,21 @@ +import { useColorScheme } from 'nativewind'; +import { useEffect, type PropsWithChildren } from 'react'; + +export function ThemeProvider({ children }: PropsWithChildren) { + useWorkaroundForThemeClass(); + + return children; +} + +function useWorkaroundForThemeClass() { + const { colorScheme } = useColorScheme(); + + useEffect(() => { + if (document.body) { + document.body.classList.remove('light-theme', 'dark-theme'); + if (colorScheme) { + document.body.className = `${colorScheme}-theme`; + } + } + }, [colorScheme]); +} diff --git a/webui/src/ui/Spinner.tsx b/webui/src/ui/Spinner.tsx new file mode 100644 index 0000000..9784194 --- /dev/null +++ b/webui/src/ui/Spinner.tsx @@ -0,0 +1,8 @@ +import cn from 'classnames'; +// @ts-expect-error +import LoaderIcon from 'lucide-react/dist/esm/icons/loader-2'; +import { type ComponentProps } from 'react'; + +export function Spinner({ className, ...props }: ComponentProps) { + return ; +}
Open your app in the browser, or device, to collect the stats.
Make sure you configured Expo Atlas properly.