Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 6,676 Bytes
1cce69a 3c9ef83 1cce69a d18f742 1cce69a 66cec88 8aa4e8b 66cec88 633c0de d18f742 633c0de 66cec88 1cce69a c4e0821 633c0de c4e0821 633c0de c4e0821 1cce69a d18f742 1cce69a 66cec88 d18f742 66cec88 8aa4e8b 633c0de 8aa4e8b 1cce69a eb12d9b d18f742 1cce69a d18f742 1cce69a 3c9ef83 d18f742 3c9ef83 d18f742 3c9ef83 d18f742 3c9ef83 d18f742 3c9ef83 d18f742 3c9ef83 d18f742 3c9ef83 d18f742 3c9ef83 a4f19d4 3c9ef83 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | "use client";
import React, { useEffect, useRef, useState } from "react";
import { useAuth } from "@/context/auth-context";
const SIGNIN_BADGE_URL =
"https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-md-dark.svg";
// `badge` β the official HF brand badge. Use as a strong invitation when the
// auth path is itself the page's headline action.
// `ghost` β a quiet inline cyan link, sized to the surrounding body copy.
// Use when auth is a secondary affordance next to a primary CTA
// (e.g. the home page's search bar).
// `tab` β uppercase tracked text styled to match a tab strip; pairs with
// the episode viewer's tab bar so the auth control reads as part
// of the same register.
type Variant = "badge" | "ghost" | "tab";
// Slot height per variant. Matches the variant's rendered button so the
// pre-config placeholder (when isAuthAvailable hasn't resolved yet) and the
// signed-in/signed-out states all occupy exactly the same vertical space β
// no layout shift on auth state changes. `tab` is taller because it lives
// in the episode tab bar and needs to align with the `text-xs px-5 py-3`
// tab buttons (~40px implicit height).
const SLOT_HEIGHT: Record<Variant, string> = {
badge: "h-8",
ghost: "h-7",
tab: "h-10",
};
interface HfAuthButtonProps {
variant?: Variant;
}
export default function HfAuthButton({ variant = "badge" }: HfAuthButtonProps) {
const { oauth, isAuthAvailable, signIn, signOut } = useAuth();
// Stable slot β auth state resolves async on mount (config fetch, then
// localStorage rehydrate), so the rendered control changes from
// null β signed-out β signed-in. Reserve the height so the surrounding
// layout doesn't reflow each time.
if (!isAuthAvailable) {
return (
<span aria-hidden className={`inline-block ${SLOT_HEIGHT[variant]}`} />
);
}
if (oauth) {
const name =
oauth.userInfo?.preferred_username ?? oauth.userInfo?.name ?? "signed in";
const avatar = oauth.userInfo?.picture;
return (
<SignedInMenu
name={name}
avatar={avatar}
onSignOut={signOut}
variant={variant}
/>
);
}
if (variant === "ghost") {
return (
<button
onClick={signIn}
title="Sign in to access your private datasets"
className="cursor-pointer inline-flex items-center h-7 gap-1.5 text-sm tracking-wide text-cyan-300/85 hover:text-cyan-200 transition-colors rounded focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
>
<span aria-hidden>π€</span>
<span>Sign in for private datasets</span>
<span aria-hidden className="opacity-60">
β
</span>
</button>
);
}
if (variant === "tab") {
return (
<button
onClick={signIn}
title="Sign in to access your private datasets"
className="cursor-pointer inline-flex items-center h-10 gap-1.5 px-5 text-[11px] font-medium tracking-wide uppercase text-slate-400 hover:text-cyan-300 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
>
<span aria-hidden>π€</span>
<span>Sign in</span>
</button>
);
}
return (
<button
onClick={signIn}
title="Sign in with Hugging Face to access your private datasets"
aria-label="Sign in with Hugging Face to access your private datasets"
className="cursor-pointer inline-flex items-center h-8 rounded-md transition-all duration-150 hover:opacity-90 motion-safe:hover:-translate-y-px focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={SIGNIN_BADGE_URL}
alt="Sign in with Hugging Face"
height={32}
className="h-8 w-auto"
/>
</button>
);
}
function SignedInMenu({
name,
avatar,
onSignOut,
variant,
}: {
name: string;
avatar?: string;
onSignOut: () => void;
variant: Variant;
}) {
const [open, setOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onDown = (e: MouseEvent) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(e.target as Node)
) {
setOpen(false);
}
};
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("mousedown", onDown);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onDown);
document.removeEventListener("keydown", onKey);
};
}, [open]);
return (
<div ref={wrapperRef} className="relative inline-flex">
<button
onClick={() => setOpen((v) => !v)}
aria-haspopup="menu"
aria-expanded={open}
className={`cursor-pointer inline-flex items-center ${SLOT_HEIGHT[variant]} gap-2 panel-raised bg-[var(--surface-0)]/85 backdrop-blur px-2 text-xs text-slate-300 hover:bg-white/[0.04] transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60`}
title={`Signed in as ${name}`}
>
{avatar && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatar}
alt=""
width={22}
height={22}
className="rounded-full ring-1 ring-white/10"
/>
)}
<span className="tabular max-w-[10rem] truncate">{name}</span>
<svg
aria-hidden
width="9"
height="9"
viewBox="0 0 8 8"
className={`text-slate-500 transition-transform ${open ? "rotate-180" : ""}`}
>
<path d="M1 2.5l3 3 3-3" stroke="currentColor" fill="none" />
</svg>
</button>
{open && (
<div
role="menu"
className="absolute right-0 top-full mt-1.5 min-w-[10rem] panel-raised bg-[var(--surface-1)]/98 backdrop-blur shadow-xl p-1 z-50 text-xs animate-menu-pop"
>
<button
role="menuitem"
onClick={() => {
setOpen(false);
onSignOut();
}}
className="cursor-pointer w-full text-left px-2 py-1.5 rounded text-slate-300 hover:bg-white/5 hover:text-slate-100 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-cyan-400/60"
>
Sign out
</button>
</div>
)}
</div>
);
}
|