Commit

Sidebar: no settings flash on the way into a repository, and a better slide

The repository menu is built from the address being gone to, so it is right from the first frame instead of the settings menu standing in until the page loads; its counts fill in when they arrive. The menus are two layers now: the arriving one slides in over the full width while the leaving one drifts a quarter of the way and fades, and moving between two far-side menus crossfades in place.

syntaqxcommitted Parentc07016aBrowse files
2 files+82−230/2 viewed
+12−0
218218 background: radial-gradient(circle at 70% 30%, rgb(182 168 255 / 0.12), rgb(255 189 140 / 0.04) 45%, transparent 70%);
219219 box-shadow: inset 14px -8px 50px rgb(255 214 186 / 0.07);
220220 }
221+
222+/* The sidebar trading one far-side menu for another, in place. */
223+@keyframes g1t-swap {
224+ from {
225+ opacity: 0;
226+ transform: translateX(10px);
227+ }
228+ to {
229+ opacity: 1;
230+ transform: none;
231+ }
232+}
+70−23
254254 const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets)(\/|$)/;
255255
256256 /**
257− * The sidebar's menus sit side by side on one track, and the track slides:
258− * the main menu leaves to the left as the settings arrive from the right,
259− * together, with a long ease-out, the way a phone pushes a screen.
257+ * The sidebar's menus are two layers, the way a phone pushes a screen: the
258+ * one arriving slides in from the right over the full width, while the one
259+ * leaving drifts a quarter of the way left and fades, both on one long
260+ * ease-out. Going back reverses it.
260261 */
261−const TRACK = "flex h-full w-[200%] transition-transform duration-[380ms] ease-[cubic-bezier(0.45,0,0.15,1)] will-change-transform motion-reduce:transition-none";
262−/** One menu on the track. */
263−const PANEL = "h-full w-1/2 shrink-0 overflow-y-auto px-2 pb-4";
262+const LAYER =
263+ "absolute inset-0 transition-[transform,opacity] duration-[380ms] ease-[cubic-bezier(0.32,0.72,0,1)] will-change-transform motion-reduce:transition-none";
264+/** One menu, filling its layer. */
265+const PANEL = "h-full overflow-y-auto px-2 pb-4";
264266
265267 /** A workspace's settings, as the sidebar shows them in place of everything else. */
266268 function SettingsMenu({ slug, owner, open }: { slug: string; owner: boolean; open: boolean }) {
306308 );
307309 }
308310
309−type ActiveRepo = NonNullable<ShellData["repo"]>;
311+/**
312+ * What the repository menu needs. Known from the address before the page's
313+ * data arrives, so the menu is right from the first frame; the counts fill
314+ * in once it does.
315+ */
316+type MenuRepo = {
317+ namespace: string;
318+ name: string;
319+ member: boolean;
320+ issues?: number;
321+ pulls?: number;
322+};
323+
324+function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
325+ return (
326+ a != null &&
327+ b != null &&
328+ a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
329+ a.name.toLowerCase() === b.name.toLowerCase()
330+ );
331+}
310332
311333 /**
312334 * A repository's own menu, which the sidebar slides to while you are in
313335 * it, as it does for settings: everything about the repository and nothing
314336 * else, with the way back to everything.
315337 */
316−function RepoMenu({ repo, isPrivate, open }: { repo: ActiveRepo; isPrivate: boolean; open: boolean }) {
338+function RepoMenu({ repo, isPrivate, open }: { repo: MenuRepo; isPrivate: boolean; open: boolean }) {
317339 const base = `/${repo.namespace}/${repo.name}`;
318340 return (
319341 <nav aria-label={`${repo.namespace}/${repo.name}`} inert={!open} className={PANEL}>
430452 const reserved = new Set(["settings", "explore", "search", "new", "workspaces", "login", "logout", "register", "verify", "forgot", "reset", "device", "oauth"]);
431453 const inRepo = repoPath != null && !reserved.has(repoPath[1]) && repoPath[2] !== "-";
432454 const away = inSettings || inAccount || inRepo;
455+ // The repository the menu is for: the one loaded if it is the one being
456+ // gone to, else what the address says, at once.
457+ const targetRepo = inRepo && repoPath ? { namespace: repoPath[1], name: repoPath[2] } : null;
458+ const menuRepo: MenuRepo | null = targetRepo
459+ ? sameRepo(active, targetRepo)
460+ ? active
461+ : {
462+ ...targetRepo,
463+ member: (user.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
464+ }
465+ : null;
433466 // What sits on the far side of the track. Kept while sliding back, so it
434467 // does not vanish on the way out.
435468 const side = useRef<"workspace" | "account" | "repo">("workspace");
437470 else if (inSettings) side.current = "workspace";
438471 else if (inRepo) side.current = "repo";
439472 // The repository last shown, kept for the slide back.
440− const shown = useRef(active);
441− if (active) shown.current = active;
473+ const shown = useRef<MenuRepo | null>(menuRepo);
474+ if (menuRepo) shown.current = menuRepo;
475+ // Moving between two of the far-side menus (settings to a repository,
476+ // one repository to another) crossfades in place instead of sliding.
477+ const detailKey = side.current === "repo" ? `repo:${shown.current?.namespace}/${shown.current?.name}` : side.current;
478+ const lastAway = useRef(away);
479+ const lastKey = useRef(detailKey);
480+ const swapped = lastAway.current && away && lastKey.current !== detailKey;
481+ useEffect(() => {
482+ lastAway.current = away;
483+ lastKey.current = detailKey;
484+ });
442485 // The repository being looked at is listed even when it is someone else's.
443486 const listed =
444487 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
467510 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
468511 </button>
469512 </div>
470− <div className="min-h-0 grow overflow-hidden">
471− <div className={`${TRACK} ${away ? "-translate-x-1/2" : "translate-x-0"}`}>
513+ <div className="relative min-h-0 grow overflow-hidden">
514+ <div className={`${LAYER} ${away ? "pointer-events-none -translate-x-1/4 opacity-0" : "translate-x-0 opacity-100"}`}>
472515 <nav aria-label="g1t" inert={away} className={PANEL}>
473516 <div className="mt-3 space-y-px">
474517 <SidebarLink to="/" end icon={<LayoutDashboard size={15} />}>
516559 })}
517560 </SidebarGroup>
518561 </nav>
519− {side.current === "repo" && shown.current ? (
520− <RepoMenu
521− repo={shown.current}
522− isPrivate={shell.repos.some((repo) => repo.namespace === shown.current!.namespace && repo.name === shown.current!.name && repo.isPrivate)}
523− open={inRepo}
524− />
525− ) : side.current === "account" || !ws ? (
526− <AccountSettingsMenu open={inAccount} />
527− ) : (
528− <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} open={inSettings} />
529− )}
530562 </div>
563+ <div className={`${LAYER} ${away ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}>
564+ <div key={detailKey} className={`h-full ${swapped ? "animate-[g1t-swap_220ms_ease-out]" : ""}`}>
565+ {side.current === "repo" && shown.current ? (
566+ <RepoMenu
567+ repo={shown.current}
568+ isPrivate={shell.repos.some((repo) => sameRepo(repo, shown.current) && repo.isPrivate)}
569+ open={inRepo}
570+ />
571+ ) : side.current === "account" || !ws ? (
572+ <AccountSettingsMenu open={inAccount} />
573+ ) : (
574+ <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} open={inSettings} />
575+ )}
576+ </div>
577+ </div>
531578 </div>
532579
533580 {/* The workspace's own things sit at the bottom, by its usage and the account. */}