mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-09 22:33:37 +00:00
refactor(web): align auth surfaces with the install wizard
Extract the wizard's INPUT_CLASS, PRIMARY_BUTTON_CLASS, SECONDARY_BUTTON_CLASS, and ErrorMessage into apps/fabro-web/app/components/ui.tsx so auth-login, setup, and the install wizard share one source of truth. - auth-login: raise the heading to text-2xl, swap white-on-teal for navy-on-teal, replace the bordered dev-token input with the outline pattern, use the ErrorMessage pill for invalid tokens, associate the input with a label, and shrink the GitHub mark to size-4 per the icons guideline - setup: replace the nested bg-overlay cards with a numbered <ol> matching the wizard's welcome layout, raise the heading, switch the primary button to navy-on-teal - install-app: re-import the shared primitives instead of holding local duplicates Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
43b30f386a
commit
2ff71d74bb
4 changed files with 158 additions and 98 deletions
23
apps/fabro-web/app/components/ui.tsx
Normal file
23
apps/fabro-web/app/components/ui.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Shared UI primitives. The install wizard set the visual baseline; this file
|
||||
// exposes the primary button, secondary button, input, and inline error
|
||||
// treatments so the auth and in-app surfaces can match.
|
||||
|
||||
export const INPUT_CLASS =
|
||||
"block w-full rounded-lg bg-panel-alt px-3.5 py-2.5 text-base text-fg outline-1 -outline-offset-1 outline-white/10 placeholder:text-fg-muted focus:outline-2 focus:-outline-offset-1 focus:outline-teal-500 sm:text-sm";
|
||||
|
||||
export const PRIMARY_BUTTON_CLASS =
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg bg-teal-500 px-4 py-2 text-sm font-medium text-navy-950 transition-colors hover:bg-teal-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-teal-500";
|
||||
|
||||
export const SECONDARY_BUTTON_CLASS =
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg bg-transparent px-3.5 py-2 text-sm font-medium text-fg-2 outline-1 -outline-offset-1 outline-white/10 hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500";
|
||||
|
||||
export function ErrorMessage({ message }: { message: string }) {
|
||||
return (
|
||||
<p
|
||||
role="alert"
|
||||
className="rounded-md bg-coral/10 px-3 py-2 text-sm/6 text-fg-2 outline-1 -outline-offset-1 outline-coral/40"
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
|
@ -37,6 +37,12 @@ import {
|
|||
consumeInstallTokenFromUrl,
|
||||
shouldConsumeInstallGithubErrorForPath,
|
||||
} from "./mode";
|
||||
import {
|
||||
ErrorMessage,
|
||||
INPUT_CLASS,
|
||||
PRIMARY_BUTTON_CLASS,
|
||||
SECONDARY_BUTTON_CLASS,
|
||||
} from "./components/ui";
|
||||
|
||||
const INSTALL_STEPS = [
|
||||
{ id: "welcome", label: "Welcome", href: "/install/welcome" },
|
||||
|
|
@ -1416,17 +1422,6 @@ function Spinner({ className = "" }: { className?: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ErrorMessage({ message }: { message: string }) {
|
||||
return (
|
||||
<p
|
||||
role="alert"
|
||||
className="rounded-md bg-coral/10 px-3 py-2 text-sm/6 text-fg-2 outline-1 -outline-offset-1 outline-coral/40"
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function defaultProviderSelection(): ProviderSelection {
|
||||
return Object.fromEntries(
|
||||
INSTALL_PROVIDERS.map((provider) => [provider.id, { apiKey: "" }]),
|
||||
|
|
@ -1509,11 +1504,3 @@ function submitGithubManifest(
|
|||
form.submit();
|
||||
}
|
||||
|
||||
const INPUT_CLASS =
|
||||
"block w-full rounded-lg bg-panel-alt px-3.5 py-2.5 text-base text-fg outline-1 -outline-offset-1 outline-white/10 placeholder:text-fg-muted focus:outline-2 focus:-outline-offset-1 focus:outline-teal-500 sm:text-sm";
|
||||
|
||||
const PRIMARY_BUTTON_CLASS =
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg bg-teal-500 px-4 py-2 text-sm font-medium text-navy-950 transition-colors hover:bg-teal-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:bg-teal-500";
|
||||
|
||||
const SECONDARY_BUTTON_CLASS =
|
||||
"inline-flex items-center justify-center gap-2 rounded-lg bg-transparent px-3.5 py-2 text-sm font-medium text-fg-2 outline-1 -outline-offset-1 outline-white/10 hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { AuthLayout } from "../components/auth-layout";
|
||||
import {
|
||||
ErrorMessage,
|
||||
INPUT_CLASS,
|
||||
PRIMARY_BUTTON_CLASS,
|
||||
} from "../components/ui";
|
||||
import { getAuthConfig, loginDevToken } from "../api";
|
||||
|
||||
export async function loader() {
|
||||
|
|
@ -23,16 +28,16 @@ export default function AuthLogin({ loaderData }: any) {
|
|||
await loginDevToken(token);
|
||||
navigate("/runs");
|
||||
} catch {
|
||||
setError("Invalid dev token");
|
||||
setError("Invalid dev token.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<h1 className="text-center text-lg font-semibold text-fg">
|
||||
<h1 className="text-center text-2xl font-semibold tracking-tight text-fg text-balance sm:text-[1.75rem]">
|
||||
Sign in to Fabro
|
||||
</h1>
|
||||
<p className="mt-2 text-center text-sm text-fg-3">
|
||||
<p className="mt-3 text-center text-sm/6 text-fg-3 text-pretty">
|
||||
{hasGitHub
|
||||
? "Authenticate with your GitHub account to continue."
|
||||
: "Paste your dev token to continue."}
|
||||
|
|
@ -41,7 +46,7 @@ export default function AuthLogin({ loaderData }: any) {
|
|||
{hasGitHub ? (
|
||||
<a
|
||||
href="/auth/login/github"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-teal-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-teal-300"
|
||||
className={`${PRIMARY_BUTTON_CLASS} w-full`}
|
||||
>
|
||||
<GitHubMark />
|
||||
Sign in with GitHub
|
||||
|
|
@ -55,34 +60,65 @@ export default function AuthLogin({ loaderData }: any) {
|
|||
onSubmit={handleSubmit}
|
||||
/>
|
||||
) : hasDevToken ? (
|
||||
<form className="space-y-3" onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(event) => setToken(event.target.value)}
|
||||
placeholder="fabro_dev_..."
|
||||
className="w-full rounded-lg border border-line-strong bg-panel px-4 py-2.5 text-sm text-fg outline-none focus:border-teal-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full items-center justify-center rounded-lg bg-teal-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-teal-300"
|
||||
>
|
||||
Sign in with Dev Token
|
||||
</button>
|
||||
<p className="text-center text-xs text-fg-muted">
|
||||
Paste the dev token from your terminal or{" "}
|
||||
<code>cat ~/.fabro/dev-token</code>
|
||||
</p>
|
||||
{error ? (
|
||||
<p className="text-center text-sm text-red-500">{error}</p>
|
||||
) : null}
|
||||
</form>
|
||||
<DevTokenForm
|
||||
token={token}
|
||||
setToken={setToken}
|
||||
error={error}
|
||||
onSubmit={handleSubmit}
|
||||
showLocation
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function DevTokenForm({
|
||||
token,
|
||||
setToken,
|
||||
error,
|
||||
onSubmit,
|
||||
showLocation = false,
|
||||
}: {
|
||||
token: string;
|
||||
setToken: (v: string) => void;
|
||||
error: string | null;
|
||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
showLocation?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<form className="space-y-4" onSubmit={onSubmit}>
|
||||
<div>
|
||||
<label htmlFor="dev-token" className="sr-only">
|
||||
Dev token
|
||||
</label>
|
||||
<input
|
||||
id="dev-token"
|
||||
type="password"
|
||||
name="dev_token"
|
||||
value={token}
|
||||
onChange={(event) => setToken(event.target.value)}
|
||||
placeholder="fabro_dev_…"
|
||||
className={`${INPUT_CLASS} font-mono`}
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
{error ? <ErrorMessage message={error} /> : null}
|
||||
<button type="submit" className={`${PRIMARY_BUTTON_CLASS} w-full`}>
|
||||
Sign in with dev token
|
||||
</button>
|
||||
{showLocation ? (
|
||||
<p className="text-center text-xs text-fg-muted">
|
||||
Paste the dev token from your terminal or{" "}
|
||||
<code className="font-mono text-fg-3">cat ~/.fabro/dev-token</code>.
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function DevTokenCollapsible({
|
||||
token,
|
||||
setToken,
|
||||
|
|
@ -101,7 +137,7 @@ function DevTokenCollapsible({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="mx-auto flex items-center gap-1 text-xs text-fg-3 hover:text-fg-2 transition-colors"
|
||||
className="mx-auto flex items-center gap-1 rounded text-xs text-fg-3 outline-teal-500 hover:text-fg-2 focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
>
|
||||
Use a dev token instead
|
||||
<svg
|
||||
|
|
@ -110,6 +146,7 @@ function DevTokenCollapsible({
|
|||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
className={`transition-transform ${open ? "rotate-180" : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M3 4.5L6 7.5L9 4.5"
|
||||
|
|
@ -121,24 +158,14 @@ function DevTokenCollapsible({
|
|||
</svg>
|
||||
</button>
|
||||
{open ? (
|
||||
<form className="mt-3 space-y-3" onSubmit={onSubmit}>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={(event) => setToken(event.target.value)}
|
||||
placeholder="fabro_dev_..."
|
||||
className="w-full rounded-lg border border-line-strong bg-panel px-4 py-2.5 text-sm text-fg outline-none focus:border-teal-500"
|
||||
<div className="mt-4">
|
||||
<DevTokenForm
|
||||
token={token}
|
||||
setToken={setToken}
|
||||
error={error}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full items-center justify-center rounded-lg border border-line-strong bg-panel px-4 py-2.5 text-sm font-medium text-fg transition-colors hover:bg-panel-2"
|
||||
>
|
||||
Sign in with Dev Token
|
||||
</button>
|
||||
{error ? (
|
||||
<p className="text-center text-sm text-red-500">{error}</p>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -146,7 +173,7 @@ function DevTokenCollapsible({
|
|||
|
||||
function GitHubMark() {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" className="size-4 shrink-0" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,45 +1,68 @@
|
|||
import { AuthLayout } from "../components/auth-layout";
|
||||
import { PRIMARY_BUTTON_CLASS } from "../components/ui";
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "Open a terminal on the server host",
|
||||
body: (
|
||||
<p className="text-sm/6 text-fg-3">
|
||||
Run{" "}
|
||||
<code className="font-mono text-fg-2">fabro install</code> on the same
|
||||
host that runs the Fabro server.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Choose GitHub App setup",
|
||||
body: (
|
||||
<p className="text-sm/6 text-fg-3">
|
||||
The CLI opens GitHub, exchanges the manifest code, and writes the
|
||||
required settings and secrets locally.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Restart the server, then return to sign in",
|
||||
body: (
|
||||
<p className="text-sm/6 text-fg-3">
|
||||
Once the server comes back up, you can authenticate from the browser.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function Setup() {
|
||||
return (
|
||||
<AuthLayout footer="GitHub App setup is managed from the terminal, not the browser.">
|
||||
<h1 className="text-center text-lg font-semibold text-fg">
|
||||
<h1 className="text-center text-2xl font-semibold tracking-tight text-fg text-balance sm:text-[1.75rem]">
|
||||
Set up Fabro
|
||||
</h1>
|
||||
<p className="mt-2 text-center text-sm text-fg-3">
|
||||
Run the installer on the same host that runs the Fabro server to
|
||||
register a GitHub App and write local configuration.
|
||||
<p className="mt-3 text-center text-sm/6 text-fg-3 text-pretty">
|
||||
Run the installer on the server host to register a GitHub App and write
|
||||
local configuration.
|
||||
</p>
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="rounded-lg border border-line-strong bg-overlay px-4 py-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-fg-muted">
|
||||
1. Open a terminal on the server host
|
||||
</p>
|
||||
<pre className="mt-2 overflow-x-auto text-sm text-fg-2">
|
||||
<code>fabro install</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="rounded-lg border border-line-strong bg-overlay px-4 py-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-fg-muted">
|
||||
2. Choose GitHub App setup
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-fg-3">
|
||||
The CLI opens GitHub, exchanges the manifest code, and writes the
|
||||
required settings and secrets locally.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-line-strong bg-overlay px-4 py-3">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-fg-muted">
|
||||
3. Restart the server, then return to sign in
|
||||
</p>
|
||||
<a
|
||||
href="/login"
|
||||
className="mt-3 flex w-full items-center justify-center rounded-lg bg-teal-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-teal-300"
|
||||
>
|
||||
Continue to sign in
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<ol
|
||||
role="list"
|
||||
className="mt-8 divide-y divide-line border-y border-line"
|
||||
>
|
||||
{steps.map((step, index) => (
|
||||
<li key={step.title} className="flex items-start gap-4 py-4">
|
||||
<span
|
||||
className="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-overlay text-xs font-semibold tabular-nums text-fg-2 outline-1 -outline-offset-1 outline-white/10"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-fg">{step.title}</p>
|
||||
<div className="mt-1">{step.body}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<a href="/login" className={`${PRIMARY_BUTTON_CLASS} mt-8 w-full`}>
|
||||
Continue to sign in
|
||||
</a>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue