diff --git a/packages/app/src/screens/startup-splash-screen.tsx b/packages/app/src/screens/startup-splash-screen.tsx
index 1d0f14bdd..5c8b7ba9e 100644
--- a/packages/app/src/screens/startup-splash-screen.tsx
+++ b/packages/app/src/screens/startup-splash-screen.tsx
@@ -1,15 +1,25 @@
import { useCallback, useEffect, useMemo, useState } from "react";
-import { ActivityIndicator, ScrollView, Text, View } from "react-native";
+import { ScrollView, Text, View } from "react-native";
+import Animated, {
+ cancelAnimation,
+ Easing,
+ useAnimatedStyle,
+ useSharedValue,
+ withRepeat,
+ withTiming,
+} from "react-native-reanimated";
+import MaskedView from "@react-native-masked-view/masked-view";
+import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg";
import * as Clipboard from "expo-clipboard";
import { openExternalUrl } from "@/utils/open-external-url";
-import { BookOpen, Check, Copy, RotateCw, TriangleAlert } from "lucide-react-native";
+import { BookOpen, Copy, RotateCw, TriangleAlert } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { PaseoLogo } from "@/components/icons/paseo-logo";
import { Button } from "@/components/ui/button";
import { Fonts } from "@/constants/theme";
import { getDesktopDaemonLogs, type DesktopDaemonLogs } from "@/desktop/daemon/desktop-daemon";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
-import { isWeb } from "@/constants/platform";
+import { isNative, isWeb } from "@/constants/platform";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
interface StartupSplashScreenProps {
@@ -23,6 +33,10 @@ interface StartupSplashScreenProps {
const GITHUB_ISSUE_URL = "https://github.com/getpaseo/paseo/issues/new";
const DOCS_URL = "https://paseo.sh/docs";
+const LOGO_SIZE = 96;
+const SHIMMER_PEAK_WIDTH = 120;
+const SHIMMER_DURATION_MS = 1800;
+
function openGithubIssue(): void {
void openExternalUrl(GITHUB_ISSUE_URL);
}
@@ -31,6 +45,141 @@ function openDocs(): void {
void openExternalUrl(DOCS_URL);
}
+const WEB_SPLASH_SHIMMER_KEYFRAME_ID = "paseo-splash-shimmer-keyframes";
+const WEB_SPLASH_SHIMMER_ANIMATION_NAME = "paseo-splash-shimmer";
+
+const WEB_SPLASH_SHIMMER_KEYFRAME_CSS = `
+ @keyframes ${WEB_SPLASH_SHIMMER_ANIMATION_NAME} {
+ 0% {
+ background-position: -${LOGO_SIZE + SHIMMER_PEAK_WIDTH}px 0;
+ }
+ 100% {
+ background-position: ${LOGO_SIZE + SHIMMER_PEAK_WIDTH}px 0;
+ }
+ }
+`;
+
+let webSplashShimmerRegistered = false;
+
+function ensureWebSplashShimmerKeyframes() {
+ if (isNative) {
+ return;
+ }
+ if (webSplashShimmerRegistered) {
+ return;
+ }
+ const existing = document.getElementById(WEB_SPLASH_SHIMMER_KEYFRAME_ID);
+ if (existing) {
+ webSplashShimmerRegistered = true;
+ return;
+ }
+ const styleElement = document.createElement("style");
+ styleElement.id = WEB_SPLASH_SHIMMER_KEYFRAME_ID;
+ styleElement.textContent = WEB_SPLASH_SHIMMER_KEYFRAME_CSS;
+ document.head.appendChild(styleElement);
+ webSplashShimmerRegistered = true;
+}
+
+function LogoShimmer() {
+ const { theme } = useUnistyles();
+
+ if (isWeb) {
+ return ;
+ }
+
+ return ;
+}
+
+function WebLogoShimmer({ color }: { color: string }) {
+ useEffect(() => {
+ ensureWebSplashShimmerKeyframes();
+ }, []);
+
+ const shimmerStyle = useMemo(
+ () => ({
+ width: LOGO_SIZE,
+ height: LOGO_SIZE,
+ WebkitMaskImage: `url("data:image/svg+xml,${encodeURIComponent(``)}")`,
+ WebkitMaskSize: "contain",
+ WebkitMaskRepeat: "no-repeat",
+ WebkitMaskPosition: "center",
+ background: `linear-gradient(90deg, ${color} 0%, ${color}88 40%, ${color}FF 50%, ${color}88 60%, ${color} 100%)`,
+ backgroundSize: `${LOGO_SIZE + SHIMMER_PEAK_WIDTH * 2}px ${LOGO_SIZE}px`,
+ animationName: WEB_SPLASH_SHIMMER_ANIMATION_NAME,
+ animationDuration: `${SHIMMER_DURATION_MS}ms`,
+ animationTimingFunction: "linear",
+ animationIterationCount: "infinite",
+ }),
+ [color],
+ );
+
+ return ;
+}
+
+function NativeLogoShimmer({ color }: { color: string }) {
+ const shimmerTranslateX = useSharedValue(-SHIMMER_PEAK_WIDTH);
+
+ useEffect(() => {
+ shimmerTranslateX.value = -SHIMMER_PEAK_WIDTH;
+ shimmerTranslateX.value = withRepeat(
+ withTiming(LOGO_SIZE + SHIMMER_PEAK_WIDTH, {
+ duration: SHIMMER_DURATION_MS,
+ easing: Easing.linear,
+ }),
+ -1,
+ false,
+ );
+ return () => {
+ cancelAnimation(shimmerTranslateX);
+ };
+ }, [shimmerTranslateX]);
+
+ const peakStyle = useAnimatedStyle(() => ({
+ transform: [{ translateX: shimmerTranslateX.value }],
+ }));
+
+ const trackStyle = useMemo(
+ () => [styles.nativeShimmerTrack, { width: LOGO_SIZE, height: LOGO_SIZE }],
+ [],
+ );
+
+ const peakCombinedStyle = useMemo(
+ () => [styles.nativeShimmerPeak, peakStyle, { width: SHIMMER_PEAK_WIDTH, height: LOGO_SIZE }],
+ [peakStyle],
+ );
+
+ const maskElement = useMemo(
+ () => (
+
+
+
+ ),
+ [],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
const styles = StyleSheet.create((theme) => ({
container: {
position: "relative",
@@ -41,10 +190,6 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[8],
paddingVertical: theme.spacing[8],
},
- containerError: {
- justifyContent: "flex-start",
- paddingTop: theme.spacing[16],
- },
errorScreen: {
position: "relative",
flex: 1,
@@ -68,12 +213,6 @@ const styles = StyleSheet.create((theme) => ({
paddingVertical: theme.spacing[8],
paddingTop: theme.spacing[16],
},
- centeredContent: {
- alignItems: "center",
- justifyContent: "center",
- maxWidth: 520,
- width: "100%",
- },
errorContent: {
alignItems: "stretch",
maxWidth: 720,
@@ -84,43 +223,11 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "flex-start",
},
title: {
- marginTop: theme.spacing[8],
color: theme.colors.foreground,
fontSize: theme.fontSize["3xl"],
fontWeight: theme.fontWeight.semibold,
- textAlign: "center",
- },
- titleError: {
textAlign: "left",
},
- subtitleRow: {
- marginTop: theme.spacing[4],
- flexDirection: "row",
- alignItems: "center",
- gap: theme.spacing[3],
- },
- progressSteps: {
- marginTop: theme.spacing[4],
- gap: theme.spacing[3],
- width: "100%",
- },
- progressStepRow: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
- gap: theme.spacing[3],
- },
- subtitle: {
- marginTop: theme.spacing[8],
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.lg,
- textAlign: "center",
- },
- subtitleInline: {
- color: theme.colors.foregroundMuted,
- fontSize: theme.fontSize.lg,
- textAlign: "center",
- },
errorDescription: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.base,
@@ -167,10 +274,29 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[3],
flexWrap: "wrap",
},
+ shimmerMask: {
+ width: LOGO_SIZE,
+ height: LOGO_SIZE,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ nativeShimmerTrack: {
+ overflow: "hidden",
+ },
+ nativeShimmerBase: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ },
+ nativeShimmerPeak: {
+ position: "absolute",
+ top: 0,
+ bottom: 0,
+ },
}));
-const TITLE_ERROR_STYLE = [styles.title, styles.titleError];
-
export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps) {
const { theme } = useUnistyles();
const webScrollbarStyle = useWebScrollbarStyle();
@@ -186,9 +312,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
const [logsError, setLogsError] = useState(null);
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
- const phase = bootstrapState?.phase;
- const isError = phase === "error";
- const isSimpleSplash = bootstrapState === undefined;
+ const isError = bootstrapState?.phase === "error";
useEffect(() => {
if (!isError) {
@@ -229,23 +353,6 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
};
}, [isError]);
- let progressSteps: { key: string; label: string; status: "active" | "complete" }[];
- if (phase === "starting-daemon") {
- progressSteps = [
- { key: "starting-daemon", label: "Starting local server...", status: "active" },
- ];
- } else if (phase === "connecting") {
- progressSteps = [
- { key: "starting-daemon", label: "Started local server", status: "complete" },
- { key: "connecting", label: "Connecting to local server...", status: "active" },
- ];
- } else {
- progressSteps = [
- { key: "starting-daemon", label: "Started local server", status: "complete" },
- { key: "connecting", label: "Connected to local server", status: "complete" },
- ];
- }
-
const logsText = useMemo(() => {
if (isLoadingLogs) {
return "Loading daemon logs...";
@@ -283,36 +390,11 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
[theme.colors.palette.white],
);
- if (isSimpleSplash) {
- return (
-
-
-
- Connecting…
-
- );
- }
-
if (!isError) {
return (
-
-
- Welcome to Paseo
-
- {progressSteps.map((step) => (
-
- {step.status === "complete" ? (
-
- ) : (
-
- )}
- {step.label}
-
- ))}
-
-
+
);
}
@@ -328,7 +410,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
- Something went wrong
+ Something went wrong