fix: use grid/cell components

This commit is contained in:
Austin Pickett 2026-04-19 15:21:57 -04:00
parent 923539a46b
commit 60fd4b7d16
12 changed files with 434 additions and 181 deletions

View file

@ -1,3 +1,4 @@
import { Typography } from "@nous-research/ui/ui/components/typography/index";
import { useI18n } from "@/i18n/context";
/**
@ -19,9 +20,12 @@ export function LanguageSwitcher() {
>
{/* Show the *current* language's flag — tooltip advertises the click action */}
<span className="text-base leading-none">{locale === "en" ? "🇬🇧" : "🇨🇳"}</span>
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
>
{locale === "en" ? "EN" : "中文"}
</span>
</Typography>
</button>
);
}

View file

@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { ExternalLink, Copy, X, Check, Loader2 } from "lucide-react";
import { Typography } from "@nous-research/ui/ui/components/typography/index";
import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -12,9 +13,21 @@ interface Props {
onError: (msg: string) => void;
}
type Phase = "idle" | "starting" | "awaiting_user" | "submitting" | "polling" | "approved" | "error";
type Phase =
| "idle"
| "starting"
| "awaiting_user"
| "submitting"
| "polling"
| "approved"
| "error";
export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props) {
export function OAuthLoginModal({
provider,
onClose,
onSuccess,
onError,
}: Props) {
const [phase, setPhase] = useState<Phase>("starting");
const [start, setStart] = useState<OAuthStartResponse | null>(null);
const [pkceCode, setPkceCode] = useState("");
@ -81,13 +94,15 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
if (!isMounted.current) return;
if (resp.status === "approved") {
setPhase("approved");
if (pollTimer.current !== null) window.clearInterval(pollTimer.current);
if (pollTimer.current !== null)
window.clearInterval(pollTimer.current);
onSuccess(`${provider.name} connected`);
window.setTimeout(() => isMounted.current && onClose(), 1500);
} else if (resp.status !== "pending") {
setPhase("error");
setErrorMsg(resp.error_message || `Login ${resp.status}`);
if (pollTimer.current !== null) window.clearInterval(pollTimer.current);
if (pollTimer.current !== null)
window.clearInterval(pollTimer.current);
}
} catch (e) {
if (!isMounted.current) return;
@ -107,7 +122,11 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
setPhase("submitting");
setErrorMsg(null);
try {
const resp = await api.submitOAuthCode(provider.id, start.session_id, pkceCode.trim());
const resp = await api.submitOAuthCode(
provider.id,
start.session_id,
pkceCode.trim(),
);
if (!isMounted.current) return;
if (resp.ok && resp.status === "approved") {
setPhase("approved");
@ -175,14 +194,24 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
</button>
<div className="p-6 flex flex-col gap-4">
<div>
<h2 id="oauth-modal-title" className="font-mondwest text-base tracking-wider uppercase">
<Typography
as="h2"
mondwest
id="oauth-modal-title"
className="text-base tracking-wider uppercase"
>
{t.oauth.connect} {provider.name}
</h2>
{secondsLeft !== null && phase !== "approved" && phase !== "error" && (
<p className="text-xs text-muted-foreground mt-1">
{t.oauth.sessionExpires.replace("{time}", fmtTime(secondsLeft))}
</p>
)}
</Typography>
{secondsLeft !== null &&
phase !== "approved" &&
phase !== "error" && (
<p className="text-xs text-muted-foreground mt-1">
{t.oauth.sessionExpires.replace(
"{time}",
fmtTime(secondsLeft),
)}
</p>
)}
</div>
{/* ── starting ───────────────────────────────────── */}
@ -211,7 +240,10 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
/>
<div className="flex items-center gap-2 justify-between">
<a
href={(start as Extract<OAuthStartResponse, { flow: "pkce" }>).auth_url}
href={
(start as Extract<OAuthStartResponse, { flow: "pkce" }>)
.auth_url
}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
@ -219,7 +251,11 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
<ExternalLink className="h-3 w-3" />
{t.oauth.reOpenAuth}
</a>
<Button onClick={handleSubmitPkceCode} disabled={!pkceCode.trim()} size="sm">
<Button
onClick={handleSubmitPkceCode}
disabled={!pkceCode.trim()}
size="sm"
>
{t.oauth.submitCode}
</Button>
</div>
@ -243,23 +279,46 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
</p>
<div className="flex items-center justify-between gap-2 border border-border bg-secondary/30 p-4">
<code className="font-mono-ui text-2xl tracking-widest text-foreground">
{(start as Extract<OAuthStartResponse, { flow: "device_code" }>).user_code}
{
(
start as Extract<
OAuthStartResponse,
{ flow: "device_code" }
>
).user_code
}
</code>
<Button
variant="outline"
size="sm"
onClick={() =>
handleCopyUserCode(
(start as Extract<OAuthStartResponse, { flow: "device_code" }>).user_code,
(
start as Extract<
OAuthStartResponse,
{ flow: "device_code" }
>
).user_code,
)
}
className="text-xs"
>
{codeCopied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
{codeCopied ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
<a
href={(start as Extract<OAuthStartResponse, { flow: "device_code" }>).verification_url}
href={
(
start as Extract<
OAuthStartResponse,
{ flow: "device_code" }
>
).verification_url
}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
@ -302,21 +361,36 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
setStart(null);
setPkceCode("");
setPhase("starting");
api.startOAuthLogin(provider.id).then((resp) => {
if (!isMounted.current) return;
setStart(resp);
setSecondsLeft(resp.expires_in);
setPhase(resp.flow === "device_code" ? "polling" : "awaiting_user");
if (resp.flow === "pkce") {
window.open(resp.auth_url, "_blank", "noopener,noreferrer");
} else {
window.open(resp.verification_url, "_blank", "noopener,noreferrer");
}
}).catch((e) => {
if (!isMounted.current) return;
setPhase("error");
setErrorMsg(`${t.common.retry} failed: ${e}`);
});
api
.startOAuthLogin(provider.id)
.then((resp) => {
if (!isMounted.current) return;
setStart(resp);
setSecondsLeft(resp.expires_in);
setPhase(
resp.flow === "device_code"
? "polling"
: "awaiting_user",
);
if (resp.flow === "pkce") {
window.open(
resp.auth_url,
"_blank",
"noopener,noreferrer",
);
} else {
window.open(
resp.verification_url,
"_blank",
"noopener,noreferrer",
);
}
})
.catch((e) => {
if (!isMounted.current) return;
setPhase("error");
setErrorMsg(`${t.common.retry} failed: ${e}`);
});
}}
>
{t.common.retry}

View file

@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Palette, Check } from "lucide-react";
import { Typography } from "@nous-research/ui/ui/components/typography/index";
import { BUILTIN_THEMES, useTheme } from "@/themes";
import { useI18n } from "@/i18n";
import { cn } from "@/lib/utils";
@ -56,9 +57,12 @@ export function ThemeSwitcher() {
aria-haspopup="listbox"
>
<Palette className="h-3.5 w-3.5" />
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
>
{label}
</span>
</Typography>
</button>
{open && (
@ -72,9 +76,12 @@ export function ThemeSwitcher() {
)}
>
<div className="border-b border-current/20 px-3 py-2">
<span className="font-mondwest text-[0.65rem] tracking-[0.15em] uppercase text-midground/70">
<Typography
mondwest
className="text-[0.65rem] tracking-[0.15em] uppercase text-midground/70"
>
{t.theme?.title ?? "Theme"}
</span>
</Typography>
</div>
{availableThemes.map((th) => {
@ -100,13 +107,16 @@ export function ThemeSwitcher() {
{preset ? <ThemeSwatch theme={preset.name} /> : <PlaceholderSwatch />}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-mondwest text-[0.75rem] tracking-wide uppercase">
<Typography
mondwest
className="truncate text-[0.75rem] tracking-wide uppercase"
>
{th.label}
</span>
</Typography>
{th.description && (
<span className="truncate font-sans text-[0.65rem] normal-case tracking-normal text-midground/50">
<Typography className="truncate text-[0.65rem] normal-case tracking-normal text-midground/50">
{th.description}
</span>
</Typography>
)}
</div>