diff --git a/src/app/components/AuthSkeleton.tsx b/src/app/components/AuthSkeleton.tsx new file mode 100644 index 000000000..1ba659ccc --- /dev/null +++ b/src/app/components/AuthSkeleton.tsx @@ -0,0 +1,69 @@ +import React, { useId } from 'react'; + +export function AuthSkeleton() { + const id = useId().replace(/:/g, ''); + const shimmerKeyframes = ` + @keyframes shimmer-${id} { + 0% { background-position: -400px 0; } + 100% { background-position: 400px 0; } + } + `; + + const shimmer = { + background: + 'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)', + backgroundSize: '800px 100%', + animation: `shimmer-${id} 1.6s ease-in-out infinite`, + borderRadius: '4px', + } as React.CSSProperties; + + return ( + <> + +
+ {/* Card */} +
+ {/* Logo + app name */} +
+
+
+
+ + {/* Server picker */} +
+
+
+
+ + {/* Form fields */} +
+
+
+
+
+
+
+ + ); +} diff --git a/src/app/components/LobbySkeleton.tsx b/src/app/components/LobbySkeleton.tsx new file mode 100644 index 000000000..85eeac014 --- /dev/null +++ b/src/app/components/LobbySkeleton.tsx @@ -0,0 +1,111 @@ +import React, { useId } from 'react'; + +const ROOM_ROWS = [ + { w: '160px', indent: false }, + { w: '120px', indent: true }, + { w: '140px', indent: true }, + { w: '130px', indent: true }, + { w: '150px', indent: false }, + { w: '110px', indent: true }, +]; + +export function LobbySkeleton() { + const id = useId().replace(/:/g, ''); + const shimmerKeyframes = ` + @keyframes shimmer-${id} { + 0% { background-position: -400px 0; } + 100% { background-position: 400px 0; } + } + `; + + const shimmer = { + background: + 'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)', + backgroundSize: '800px 100%', + animation: `shimmer-${id} 1.6s ease-in-out infinite`, + borderRadius: '4px', + } as React.CSSProperties; + + return ( + <> + +
+ {/* Header — matches LobbyHeader (56px) */} +
+
+
+
+
+
+
+ +
+ {/* Hero — matches PageHero with large avatar + title + subtitle */} +
+
+
+
+
+ + {/* Room list rows */} +
+ {ROOM_ROWS.map((row, i) => ( + // eslint-disable-next-line react/no-array-index-key +
+
+
+
+ ))} +
+
+
+ + ); +} diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index fd3aa826d..89f1e73f3 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -557,7 +557,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli base += len; return seg; }); - }, [timeline.linkedTimelines]); + }, [timeline.linkedTimelines, eventsLength]); const liveTimelineLinked = timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room); const canPaginateBack = diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index a70faa521..66b7340fb 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -8,6 +8,8 @@ import { redirect, } from 'react-router-dom'; import { RoomSkeleton } from '../components/RoomSkeleton'; +import { LobbySkeleton } from '../components/LobbySkeleton'; +import { AuthSkeleton } from '../components/AuthSkeleton'; import { ClientConfig } from '../hooks/useClientConfig'; import { @@ -124,7 +126,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) return null; }} element={ - + }> @@ -309,7 +311,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) + }> }