From f2e089c317706cfb20cf801c7381d098246322a2 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Sun, 12 Apr 2026 16:49:09 -0400 Subject: [PATCH 1/2] Polish user menu, 404 error screen, and detail page skeletons - User dropdown: add avatar/name/username header, icons on menu items, LogOut icon export - Detail breadcrumb: make repo name a link to // - 404 errors: show app logo + "Go home" CTA instead of retry, suppress error detail block - Profile page: treat null profile response as not-found error - Detail skeletons: staggered spring entrance with looping fade-out, simplified and varied activity timeline items, merge status card placeholder --- .../src/components/details/detail-page.tsx | 158 +++++++++++++++--- .../issues/detail/issue-detail-page.tsx | 106 +++++++----- .../layouts/dashboard-error-screen.tsx | 62 ++++--- .../components/layouts/dashboard-topbar.tsx | 63 ++++--- .../pulls/detail/pull-detail-page.tsx | 132 ++++++++------- .../src/routes/_protected/$owner/index.tsx | 1 + packages/icons/src/index.ts | 1 + 7 files changed, 343 insertions(+), 180 deletions(-) diff --git a/apps/dashboard/src/components/details/detail-page.tsx b/apps/dashboard/src/components/details/detail-page.tsx index e8aa074..a88984f 100644 --- a/apps/dashboard/src/components/details/detail-page.tsx +++ b/apps/dashboard/src/components/details/detail-page.tsx @@ -1,6 +1,110 @@ import { Skeleton } from "@diffkit/ui/components/skeleton"; import { cn } from "@diffkit/ui/lib/utils"; import { Link } from "@tanstack/react-router"; +import { animate, motion, useMotionValue, useTransform } from "motion/react"; +import { createContext, useContext, useEffect, useState } from "react"; + +const STAGGER_DELAY = 1; +const ITEM_DURATION = 1.25; +const FADE_OUT_DURATION = 0.5; +const PAUSE_BEFORE_RESTART = 1; + +type StaggerContextValue = { + cycle: number; + groupOpacity: ReturnType>; +}; +const defaultGroupOpacity = { + get: () => 1, + set: () => {}, +} as unknown as ReturnType>; +const StaggerCycleContext = createContext({ + cycle: 0, + groupOpacity: defaultGroupOpacity, +}); + +function StaggerLoop({ + itemCount, + children, +}: { + itemCount: number; + children: React.ReactNode; +}) { + const [cycle, setCycle] = useState(0); + const groupOpacity = useMotionValue(1); + + // biome-ignore lint/correctness/useExhaustiveDependencies: cycle drives the restart loop + useEffect(() => { + const lastItemFinish = (itemCount - 1) * STAGGER_DELAY + ITEM_DURATION; + const totalVisible = lastItemFinish + PAUSE_BEFORE_RESTART; + + const timeout = setTimeout(() => { + const controls = animate(groupOpacity, 0, { + duration: FADE_OUT_DURATION, + ease: "easeInOut", + onComplete: () => { + groupOpacity.set(1); + setCycle((c) => c + 1); + }, + }); + return () => controls.stop(); + }, totalVisible * 1000); + + return () => clearTimeout(timeout); + }, [cycle, itemCount, groupOpacity]); + + return ( + + {children} + + ); +} + +function StaggerItem({ + children, + index, + className, +}: { + children: React.ReactNode; + index: number; + className?: string; +}) { + const { cycle, groupOpacity } = useContext(StaggerCycleContext); + const itemOpacity = useMotionValue(0); + const combinedOpacity = useTransform( + [itemOpacity, groupOpacity], + ([item, group]) => Math.min(item as number, group as number), + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: cycle resets the item animation + useEffect(() => { + itemOpacity.set(0); + const controls = animate(itemOpacity, 1, { + type: "spring", + duration: ITEM_DURATION, + bounce: 0, + delay: index * STAGGER_DELAY, + }); + return () => controls.stop(); + }, [cycle, index, itemOpacity]); + + return ( + + {children} + + ); +} type DetailHeaderIcon = React.ComponentType<{ size?: number; @@ -56,9 +160,13 @@ export function DetailPageTitle({ {collectionLabel} / - + {owner}/{repo} - + / #{number} @@ -76,32 +184,36 @@ export function DetailPageTitle({ ); } +export { StaggerItem }; + export function DetailPageSkeletonLayout({ - main, - sidebarSectionCount = 4, + children, + mainItemCount, + sidebarSectionCount = 3, }: { - main: React.ReactNode; + children: React.ReactNode; + mainItemCount: number; sidebarSectionCount?: number; }) { + const totalItems = Math.max(mainItemCount, sidebarSectionCount); return ( -
-
-
{main}
- + +
+
+
{children}
+ +
-
+ ); } diff --git a/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx b/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx index 1979bb2..32b0322 100644 --- a/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx +++ b/apps/dashboard/src/components/issues/detail/issue-detail-page.tsx @@ -4,6 +4,7 @@ import { getRouteApi } from "@tanstack/react-router"; import { DetailPageLayout, DetailPageSkeletonLayout, + StaggerItem, } from "#/components/details/detail-page"; import { githubIssuePageQueryOptions, @@ -89,58 +90,77 @@ export function IssueDetailPage() { function IssueDetailPageSkeleton() { return ( - -
- -
- -
- -
- - -
+ + +
+ +
+ +
+ +
+ +
+
+
-
- - -
+ +
+ + +
+
-
-
- - - - -
+ +
+
+ + +
+
+
-
-
- - + +
+
+ + +
+
+ {/* Comment */} +
+ +
+ + +
+
+ {/* Label event */} +
+ + + +
+ {/* Comment */} +
+ +
+ + +
-
- {["activity-1", "activity-2", "activity-3"].map((key) => ( -
-
- - - -
- - -
- ))} + {/* Assignment */} +
+ +
- - } - /> +
+ + ); } diff --git a/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx b/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx index fce7310..760ff1b 100644 --- a/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx @@ -6,8 +6,13 @@ import { WifiOffIcon, } from "@diffkit/icons"; import { Button } from "@diffkit/ui/components/button"; +import { Logo } from "@diffkit/ui/components/logo"; import { cn } from "@diffkit/ui/lib/utils"; -import { type ErrorComponentProps, useRouter } from "@tanstack/react-router"; +import { + type ErrorComponentProps, + Link, + useRouter, +} from "@tanstack/react-router"; import type { ComponentType } from "react"; import { useShowOrgSetupQueryState } from "#/lib/github-access-dialog-query"; import { openGitHubAccessPrompt } from "#/lib/github-access-modal-store"; @@ -17,7 +22,7 @@ type ErrorInfo = { iconClassName: string; title: string; description: string; - action: "retry" | "configure-access"; + action: "retry" | "configure-access" | "go-home"; }; function getErrorInfo(error: Error): ErrorInfo { @@ -58,7 +63,7 @@ function getErrorInfo(error: Error): ErrorInfo { title: "Not found", description: "This resource doesn't exist or you don't have access to it.", - action: "retry", + action: "go-home", }; } @@ -119,19 +124,24 @@ export function DashboardErrorScreen({ error, reset }: ErrorComponentProps) { description, action, } = getErrorInfo(error); - const detail = cleanErrorMessage(error.message); + const isNotFound = action === "go-home"; + const detail = isNotFound ? null : cleanErrorMessage(error.message); return (
-
- -
+ {isNotFound ? ( + + ) : ( +
+ +
+ )}

{title}

@@ -148,17 +158,23 @@ export function DashboardErrorScreen({ error, reset }: ErrorComponentProps) {
{action === "configure-access" ? : null} - + {action === "go-home" ? ( + + ) : ( + + )}
diff --git a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx index 87c2060..95fb86b 100644 --- a/apps/dashboard/src/components/layouts/dashboard-topbar.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-topbar.tsx @@ -2,11 +2,11 @@ import { GitPullRequestIcon, HomeIcon, IssuesIcon, - MoonIcon, + LogOutIcon, MoreHorizontalIcon, ReviewsIcon, - SunIcon, - SystemIcon, + SettingsIcon, + UserCircleIcon, } from "@diffkit/icons"; import { Avatar, AvatarFallback } from "@diffkit/ui/components/avatar"; import { Button } from "@diffkit/ui/components/button"; @@ -22,7 +22,6 @@ import { } from "@diffkit/ui/components/dropdown-menu"; import { useQuery } from "@tanstack/react-query"; import { Link, useRouter } from "@tanstack/react-router"; -import { useTheme } from "next-themes"; import { useEffect, useMemo, useRef, useState } from "react"; import { DashboardTabs } from "#/components/layouts/dashboard-tabs"; import { signOutToLogin } from "#/lib/auth-actions"; @@ -53,12 +52,6 @@ type NavItem = { count?: number; }; -const themeOptions = [ - { value: "light", icon: SunIcon, label: "Light" }, - { value: "dark", icon: MoonIcon, label: "Dark" }, - { value: "system", icon: SystemIcon, label: "System" }, -] as const; - const primaryNavRoutes = ["/", "/pulls", "/issues", "/reviews"] as const; const MAX_TAB_SHORTCUTS = 9; @@ -67,7 +60,6 @@ export function DashboardTopbar({ tabsReady, counts, }: DashboardTopbarProps) { - const { theme, setTheme } = useTheme(); const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); const openTabs = useTabs(); const hasMounted = useHasMounted(); @@ -209,41 +201,43 @@ export function DashboardTopbar({ - -
-

{displayName}

-

- {user.email} -

-
-
- {themeOptions.map((opt) => ( - - ))} + + + {user.image && !avatarLoadFailed ? ( + {displayName} + ) : ( + + {initials} + + )} + +
+ + {displayName} + + {viewerLogin && ( + + @{viewerLogin} + + )}
+ Profile + Settings @@ -255,6 +249,7 @@ export function DashboardTopbar({ void signOutToLogin(); }} > + Sign out diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx index f93ca87..cb597b0 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-page.tsx @@ -4,6 +4,7 @@ import { getRouteApi } from "@tanstack/react-router"; import { DetailPageLayout, DetailPageSkeletonLayout, + StaggerItem, } from "#/components/details/detail-page"; import { githubPullPageQueryOptions, @@ -119,73 +120,90 @@ export function PullDetailPage() { function PullDetailPageSkeleton() { return ( - -
- -
- -
- -
- - -
+ + +
+ +
+ +
+ +
+ +
+
+
-
- - - -
+ +
+ + + +
+
-
-
- - - - -
+ +
+
+ + +
+
+
-
-
- - + +
+
+ + +
+
+ {/* Comment */} +
+ +
+ + +
-
- {[0, 1, 2].map((item) => ( -
-
- - - -
- - -
- ))} + {/* Commit */} +
+ +
-
- {[0, 1, 2].map((item) => ( -
- -
- - -
-
- ))} + {/* Review */} +
+ +
+ + +
+
+ {/* Label event */} +
+ + +
- - } - /> +
+ + + +
+
+
+ + +
+ +
+
+
+ ); } diff --git a/apps/dashboard/src/routes/_protected/$owner/index.tsx b/apps/dashboard/src/routes/_protected/$owner/index.tsx index 4d8c70e..341905d 100644 --- a/apps/dashboard/src/routes/_protected/$owner/index.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/index.tsx @@ -89,6 +89,7 @@ function ProfilePage() { const activity = activityQuery.data?.pages.flat(); if (profileQuery.error) throw profileQuery.error; + if (profileQuery.data === null) throw new Error("Not found"); return (
diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index 5e6320b..c375ec0 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -45,6 +45,7 @@ export { Loading03Icon as LoaderCircleIcon, Location01Icon as LocationIcon, LockIcon, + Logout01Icon as LogOutIcon, Mail01Icon as MailIcon, Message01Icon as MessageIcon, Moon02Icon as MoonIcon, From 0e8166e9a258ad47a82cc926d8b696e5b47eece8 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Sun, 12 Apr 2026 16:50:09 -0400 Subject: [PATCH 2/2] Add .zed to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 46c760d..9dd06be 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ dist-ssr .vinxi __unconfig* .turbo +.zed