Skip to content

Compare changes

Choose two branches to see what one has that the other does not, then open a pull request for it.

Open a pull request

1 commit

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. */}