Brand lockup, no idle env-check card, no Mission row (#12074)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - New customers meet the product through an onboarding arc that ends
in the tenant wizard
> - A staging walk of that arc found three rough edges: an ad-hoc icon
standing in for the brand, an environment-check card that narrates a
probe the flow already runs on its own, and a review checklist that
still lists a Mission the arc stopped asking for
> - Each one makes the product look less finished than it is at the
exact moment a customer decides what it is
> - This pull request renders the brand lockup, hides the idle
environment-check card while keeping the probe and its failure surface,
and drops the Mission row
> - The benefit is a first-session arc that reads as one product, with
no controls for questions nobody was asked

## Linked Issues or Issue Description

No public issue exists. The changes come from walking the sign-up arc on
a staging fleet.

**What happened:**
The model step shows an "Adapter environment check" card with a "Test
now" button even though pressing Connect runs the same probe and blocks
a failing hire. The review step lists "Mission" in its checklist
although onboarding no longer asks for one. The auth page renders a
sparkles icon beside the word "Paperclip" instead of the brand lockup.

**Expected behavior:**
The model step shows the check only when a probe has found something to
fix. The review checklist lists only what onboarding set up. The brand
renders as the lockup asset used across surfaces.

**Actual behavior:**
An idle card narrates a probe that runs regardless. A permanent
unchecked row marks a question nobody was asked. The brand is a generic
icon plus text.

**Steps to reproduce:**
1. Sign up on a staging fleet and enter the tenant wizard.
2. On "Create your first agent", choose a role and press Next: the model
step shows the "Adapter environment check" card before anything has been
probed.
3. Continue to Review: the checklist lists "Mission" as a permanently
unchecked row.

**Additional context:**
The Mission row outlived the removal of the mission step (#11935). The
environment probe itself still runs on Connect and blocks a failing
hire; only its idle card is at issue. The brand lockup lands across all
three surfaces in the same round — paperclip-cloud#270 and
paperclip-id#58 carry the other halves.

## What Changed

- `PaperclipLockup` renders the brand asset (mark + wordmark, one
geometry, `fill="currentColor"`); the auth page uses it in place of the
sparkles icon.
- The adapter environment check's idle card (explainer + "Test now") no
longer renders. The probe still runs on Connect and still blocks a
failing hire.
- The check's failure content still renders when a probe has found
something — the blocking error points the customer at "the reported
checks", so they stay visible.
- Connect retries a cached failed probe instead of reusing it. With
"Test now" gone, Connect is the only retry, and a stale fail would lock
out a machine the customer has since fixed.
- The review checklist drops its "Mission" row.

## Verification

Run the tenant suite:

```
cd ui && npx vitest run
```

- 4365 tests pass across 471 files; `npx tsc --noEmit` is clean.
- New test drives the wizard to the model step and asserts the
environment-check card is absent, anchored on "Connect a model" so an
unrendered step cannot pass as an absence.
- The review assertions anchor on the remaining rows ("Organization
name", "Agent created", "Model connected").

## Risks

- **Behavioral change:** a cached *failed* probe is re-run on Connect
instead of reused. Pass and warn results are still cached. This only
affects the retry path that "Test now" used to serve.
- **Hidden, not removed:** the environment check machinery is intact;
only the idle card is gone. A failing probe still blocks the hire and
still shows its checks.
- **Brand:** the wordmark now ships inside an SVG; its accessible name
carries the text. Screen readers announce "Paperclip" as before.

## Model Used

Claude Fable 5 (`claude-fable-5`) via Claude Code, with tool use and
code execution; earlier rounds on this branch's predecessor used Claude
Opus 5 (`claude-opus-5`).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
Paperclip ticket id
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tonio 2026-08-24 01:35:15 -07:00 committed by GitHub
parent a14e51d592
commit 88a0f885e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 178 additions and 28 deletions

View File

@ -57,6 +57,16 @@ const mockAgentsApi = vi.hoisted(() => ({
instructionsBundle: vi.fn(async () => ({ entryFile: "AGENTS.md" })),
saveInstructionsFile: vi.fn(async () => ({})),
}));
// The Connect path loads environment settings before probing; without these
// the probe dies on "Could not load environment settings" and the hire never
// runs — which reads as a mysterious 0-call assertion, not an error.
const mockEnvironmentsApi = vi.hoisted(() => ({
list: vi.fn(async () => []),
}));
const mockInstanceSettingsApi = vi.hoisted(() => ({
get: vi.fn(async () => ({ defaultEnvironmentId: null })),
getExperimental: vi.fn(async () => ({ enableManagedSandboxOnly: false })),
}));
const mockApprovalsApi = vi.hoisted(() => ({
create: vi.fn(),
}));
@ -93,6 +103,8 @@ vi.mock("../api/agents", () => ({ agentsApi: mockAgentsApi }));
vi.mock("../api/approvals", () => ({ approvalsApi: mockApprovalsApi }));
vi.mock("../api/issues", () => ({ issuesApi: mockIssuesApi }));
vi.mock("../api/projects", () => ({ projectsApi: mockProjectsApi }));
vi.mock("../api/environments", () => ({ environmentsApi: mockEnvironmentsApi }));
vi.mock("../api/instanceSettings", () => ({ instanceSettingsApi: mockInstanceSettingsApi }));
vi.mock("../adapters", () => ({
listUIAdapters: () => mockAdapterRegistry.list,
getUIAdapter: () => ({ buildAdapterConfig: () => ({}) }),
@ -257,6 +269,96 @@ describe("OnboardingWizard restore-gate (stale localStorage across accounts)", (
await act(async () => root.unmount());
});
it("shows no environment-check card on the model step, and no Mission row on review", async () => {
// Round-3 walk feedback: the adapter environment check still runs —
// Connect probes before hiring and blocks on a fail — but its idle card
// (explainer plus "Test now") is gone. And the review checklist lost its
// "Mission" row: onboarding stopped asking, so the row could only ever
// render unchecked. Both asserted against positive anchors so an
// unrendered step cannot pass as an absence.
mockCompaniesApi.create.mockResolvedValue({ id: "company-new", issuePrefix: "INI" });
const { root } = await openStepOne("create");
await clickByText((t) => t.startsWith("Next"));
expect(document.body.textContent).toContain("Create your first agent");
// Step 3 → 4 needs an agent name; choosing a role fills it.
const roleTrigger = document.body.querySelector("#onboarding-agent-role") as HTMLElement;
await act(async () => {
roleTrigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
const ceo = [...document.body.querySelectorAll('[role="option"]')].find(
(o) => o.textContent?.trim() === "CEO",
) as HTMLElement;
await act(async () => {
ceo.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
await clickByText((t) => t.startsWith("Next"));
expect(document.body.textContent).toContain("Connect a model");
expect(document.body.textContent).not.toContain("Adapter environment check");
expect(document.body.textContent).not.toContain("Test now");
// Through Connect to Review, so the Mission-row assertion runs against
// the checklist that actually renders it — stopping at the model step
// would let a Mission regression pass unseen.
await clickByText((t) => t.startsWith("Connect"));
expect(document.body.textContent).toContain("Review");
expect(document.body.textContent).toContain("Organization name");
expect(document.body.textContent).toContain("Agent created");
expect(document.body.textContent).toContain("Model connected");
expect(document.body.textContent).not.toContain("Mission");
await act(async () => root.unmount());
});
it("hires one agent when Connect fires twice in one breath", async () => {
// The Connect handler re-runs a cached failed probe now that "Test now"
// is gone — so two overlapping submissions could both pass the fresh
// probe and both hire. `loading` cannot stop the second caller: it is
// state, unwritten while the first call is still awaiting. The ref
// guard must make the second submission a no-op.
let resolveHire: (v: { agent: { id: string }; approval: null }) => void = () => {};
mockAgentsApi.hire.mockReturnValue(
new Promise((resolve) => {
resolveHire = resolve;
}),
);
mockCompaniesApi.create.mockResolvedValue({ id: "company-new", issuePrefix: "INI" });
const { root } = await openStepOne("create");
await clickByText((t) => t.startsWith("Next"));
const roleTrigger = document.body.querySelector("#onboarding-agent-role") as HTMLElement;
await act(async () => {
roleTrigger.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
const ceo = [...document.body.querySelectorAll('[role="option"]')].find(
(o) => o.textContent?.trim() === "CEO",
) as HTMLElement;
await act(async () => {
ceo.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
await clickByText((t) => t.startsWith("Next"));
expect(document.body.textContent).toContain("Connect a model");
const connect = [...document.body.querySelectorAll("button")].find((b) =>
b.textContent?.trim().startsWith("Connect"),
)!;
await act(async () => {
connect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
connect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await flushReact();
await act(async () => resolveHire({ agent: { id: "agent-1" }, approval: null }));
await flushReact();
expect(mockAgentsApi.hire).toHaveBeenCalledTimes(1);
await act(async () => root.unmount());
});
it("creates one company for one keystroke, modifier or not", async () => {
// The name field handles Enter itself and does not check for a modifier,
// so Cmd+Enter in that field reaches the field's handler *and* the

View File

@ -461,6 +461,11 @@ function OnboardingWizardInner({
// either, because it is not set until the request it guards has resolved. A
// ref is written before the request goes out, so the second caller sees it.
const creatingCompanyRef = useRef(false);
// Same shape for the hire. Greptile (round-3 PR): with "Test now" gone the
// Connect handler re-runs a cached failed probe — and two overlapping
// submissions could then both pass the fresh probe and both hire. `loading`
// cannot stop the second caller for the same reason as above.
const hiringAgentRef = useRef(false);
createdCompanyIdRef.current = createdCompanyId;
// The mission of the company actually in hand, which is not always the one
@ -1256,6 +1261,8 @@ function OnboardingWizardInner({
setStep(5);
return;
}
if (hiringAgentRef.current) return;
hiringAgentRef.current = true;
setLoading(true);
setError(null);
try {
@ -1293,7 +1300,13 @@ function OnboardingWizardInner({
}
if (isLocalAdapter) {
const result = adapterEnvResult ?? (await runAdapterEnvironmentTest());
// A cached pass or warn is still good; a cached fail is retried. With
// the "Test now" card gone, this button is the only way to re-probe,
// and reusing a stale fail would lock a customer out of a machine
// they have since fixed.
const cachedUsable =
adapterEnvResult && adapterEnvResult.status !== "fail" ? adapterEnvResult : null;
const result = cachedUsable ?? (await runAdapterEnvironmentTest());
if (!result) return;
// Block the hire on a failed environment test. A pass or a warn may
// proceed; a fail means the agent cannot run as configured.
@ -1366,6 +1379,7 @@ function OnboardingWizardInner({
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create agent");
} finally {
hiringAgentRef.current = false;
setLoading(false);
}
}
@ -2232,29 +2246,15 @@ function OnboardingWizardInner({
</div>
)}
{isLocalAdapter && (
{/* The environment check runs without being shown: Connect
probes the adapter before hiring (see handleGiveHeartbeat)
and blocks the hire on a fail. The idle card probe
explainer plus a "Test now" button is gone from this
step, so this block renders only when a probe has actually
found something: the checks the blocking error tells the
customer to fix have to be visible somewhere. */}
{isLocalAdapter && (adapterEnvError || (adapterEnvResult && adapterEnvResult.status !== "pass")) && (
<div className="space-y-2 rounded-md border border-border p-3">
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-xs font-medium">
Adapter environment check
</p>
<p className="text-(length:--text-micro) text-muted-foreground">
Runs a live probe that asks the adapter CLI to
respond with hello.
</p>
</div>
<Button
size="sm"
variant="outline"
className="h-7 px-2.5 text-xs"
disabled={adapterEnvLoading}
onClick={() => void runAdapterEnvironmentTest()}
>
{adapterEnvLoading ? "Testing..." : "Test now"}
</Button>
</div>
{adapterEnvError && (
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-2.5 py-2 text-(length:--text-micro) text-destructive">
{adapterEnvError}
@ -2397,9 +2397,11 @@ function OnboardingWizardInner({
<div className="space-y-5 py-1">
{/* Review checklist — everything that's now set up */}
<div className="space-y-1.5">
{/* No "Mission" row: onboarding stopped asking for one, so a
checklist item for it could only ever render unchecked
a permanent red mark for a question nobody was asked. */}
{[
{ label: "Organization name", done: Boolean(companyName.trim()) },
{ label: "Mission", done: Boolean(companyGoal.trim()) },
{ label: "Agent created", done: Boolean(createdAgentId) },
{ label: "Model connected", done: Boolean(createdAgentId) },
].map(({ label, done }) => (

View File

@ -0,0 +1,47 @@
import type { SVGProps } from "react";
interface PaperclipLockupProps extends Omit<SVGProps<SVGSVGElement>, "children"> {
decorative?: boolean;
title?: string;
}
/**
* The full Paperclip lockup mark plus wordmark from the brand asset
* (`paperclip_lockup_dark_mode alt.svg`). The export is white-filled for dark
* chrome; here every path fills `currentColor`, so the one geometry follows
* the theme the way the brand system expects ("one vocabulary, two surfaces").
* The viewBox is cropped to the artwork's content bounds the export carries
* stage padding that would render the lockup at a third of its container.
*
* Size it with a height class (`h-5 w-auto`); width follows the aspect.
*/
export function PaperclipLockup({
decorative = false,
title = "Paperclip",
className,
...rest
}: PaperclipLockupProps) {
return (
<svg
{...rest}
className={className}
viewBox="22.5 22.5 121 27"
fill="currentColor"
role={decorative ? undefined : "img"}
aria-hidden={decorative ? true : undefined}
aria-label={decorative ? undefined : title}
focusable="false"
>
<path d="M131.15 48.4902V31.9902H133.922V33.9775H134.094C134.237 33.6911 134.441 33.3831 134.706 33.0537C134.971 32.7171 135.329 32.4307 135.78 32.1943C136.239 31.958 136.822 31.8398 137.531 31.8398C138.455 31.8398 139.289 32.0762 140.034 32.5488C140.779 33.0215 141.37 33.7161 141.807 34.6328C142.251 35.5495 142.473 36.6774 142.473 38.0166C142.473 39.3343 142.258 40.4551 141.828 41.3789C141.398 42.3027 140.808 43.0081 140.056 43.4951C139.311 43.9749 138.466 44.2148 137.521 44.2148C136.84 44.2148 136.271 44.1003 135.812 43.8711C135.361 43.6348 134.996 43.3555 134.717 43.0332C134.445 42.7038 134.237 42.3923 134.094 42.0986H133.976V48.4902H131.15ZM136.747 41.8945C137.37 41.8945 137.889 41.7262 138.305 41.3896C138.727 41.0531 139.046 40.5911 139.261 40.0039C139.476 39.4167 139.583 38.7507 139.583 38.0059C139.583 37.2611 139.476 36.5986 139.261 36.0186C139.046 35.4385 138.731 34.9837 138.315 34.6543C137.9 34.3249 137.377 34.1602 136.747 34.1602C136.138 34.1602 135.623 34.3177 135.2 34.6328C134.785 34.9479 134.466 35.3955 134.244 35.9756C134.029 36.5485 133.922 37.2253 133.922 38.0059C133.922 38.7793 134.029 39.4596 134.244 40.0469C134.466 40.627 134.788 41.0817 135.211 41.4111C135.633 41.7334 136.146 41.8945 136.747 41.8945Z" />
<path d="M126.451 44V31.9902H129.276V44H126.451ZM127.869 30.3037C127.417 30.3037 127.031 30.1533 126.708 29.8525C126.386 29.5518 126.225 29.1901 126.225 28.7676C126.225 28.3379 126.386 27.9727 126.708 27.6719C127.031 27.3711 127.417 27.2207 127.869 27.2207C128.313 27.2207 128.696 27.3711 129.018 27.6719C129.34 27.9727 129.501 28.3343 129.501 28.7568C129.501 29.1865 129.34 29.5518 129.018 29.8525C128.696 30.1533 128.313 30.3037 127.869 30.3037Z" />
<path d="M124.576 27.9941V44H121.75V27.9941H124.576Z" />
<path d="M115.3 44.2363C114.104 44.2363 113.076 43.9749 112.217 43.4521C111.357 42.9222 110.699 42.1917 110.24 41.2607C109.782 40.3298 109.553 39.2591 109.553 38.0488C109.553 36.8242 109.785 35.7464 110.251 34.8154C110.716 33.8844 111.379 33.1576 112.238 32.6348C113.098 32.1048 114.111 31.8398 115.278 31.8398C116.259 31.8398 117.126 32.0189 117.878 32.377C118.63 32.735 119.228 33.2399 119.672 33.8916C120.123 34.5361 120.381 35.2917 120.445 36.1582H117.738C117.667 35.7715 117.527 35.4242 117.319 35.1162C117.112 34.8083 116.84 34.5648 116.503 34.3857C116.174 34.2067 115.78 34.1172 115.321 34.1172C114.741 34.1172 114.233 34.2747 113.796 34.5898C113.366 34.8978 113.03 35.3418 112.786 35.9219C112.543 36.502 112.421 37.1966 112.421 38.0059C112.421 38.8223 112.539 39.5277 112.775 40.1221C113.012 40.7093 113.348 41.1641 113.785 41.4863C114.222 41.8014 114.734 41.959 115.321 41.959C115.944 41.959 116.471 41.7764 116.9 41.4111C117.337 41.0459 117.617 40.541 117.738 39.8965H120.445C120.381 40.7487 120.13 41.5007 119.693 42.1523C119.257 42.804 118.666 43.3161 117.921 43.6885C117.176 44.0537 116.302 44.2363 115.3 44.2363Z" />
<path d="M102.876 44V31.9902H105.615V34.0098H105.744C105.959 33.3079 106.335 32.7708 106.872 32.3984C107.409 32.0189 108.021 31.8291 108.709 31.8291C108.859 31.8291 109.031 31.8363 109.225 31.8506C109.425 31.8649 109.59 31.8828 109.719 31.9043V34.5039C109.597 34.4609 109.407 34.4251 109.149 34.3965C108.892 34.3678 108.645 34.3535 108.408 34.3535C107.885 34.3535 107.42 34.4645 107.012 34.6865C106.604 34.9085 106.281 35.2129 106.045 35.5996C105.816 35.9863 105.701 36.4375 105.701 36.9531V44H102.876Z" />
<path d="M96.2319 44.2363C95.0288 44.2363 93.9904 43.9857 93.1167 43.4844C92.243 42.9759 91.5698 42.2598 91.0972 41.3359C90.6317 40.4121 90.3989 39.3236 90.3989 38.0703C90.3989 36.8314 90.6317 35.7464 91.0972 34.8154C91.5627 33.8773 92.2179 33.1468 93.063 32.624C93.908 32.1012 94.9035 31.8398 96.0493 31.8398C96.7869 31.8398 97.4852 31.958 98.144 32.1943C98.8101 32.4307 99.3973 32.7959 99.9058 33.29C100.421 33.7842 100.826 34.4144 101.12 35.1807C101.413 35.9398 101.56 36.8457 101.56 37.8984V38.7793H91.7202V36.8779H100.153L98.8315 37.4365C98.8315 36.7562 98.7277 36.1654 98.52 35.6641C98.3123 35.1556 98.0044 34.7581 97.5962 34.4717C97.188 34.1852 96.6795 34.042 96.0708 34.042C95.4621 34.042 94.9429 34.1852 94.5132 34.4717C94.0835 34.7581 93.7576 35.1449 93.5356 35.6318C93.3136 36.1117 93.2026 36.6488 93.2026 37.2432V38.5645C93.2026 39.2949 93.328 39.9215 93.5786 40.4443C93.8364 40.96 94.1945 41.3538 94.6528 41.626C95.1183 41.8981 95.659 42.0342 96.2749 42.0342C96.6759 42.0342 97.0412 41.9769 97.3706 41.8623C97.7072 41.7477 97.9937 41.5758 98.23 41.3467C98.4735 41.1104 98.6597 40.8203 98.7886 40.4766L101.431 40.7881C101.259 41.4827 100.941 42.0915 100.475 42.6143C100.017 43.1299 99.4259 43.5309 98.7026 43.8174C97.9865 44.0967 97.1629 44.2363 96.2319 44.2363Z" />
<path d="M78.2437 48.4902V31.9902H81.0151V33.9775H81.187C81.3302 33.6911 81.5343 33.3831 81.7993 33.0537C82.0643 32.7171 82.4224 32.4307 82.8735 32.1943C83.3319 31.958 83.9155 31.8398 84.6245 31.8398C85.5483 31.8398 86.3826 32.0762 87.1274 32.5488C87.8722 33.0215 88.4631 33.7161 88.8999 34.6328C89.3439 35.5495 89.5659 36.6774 89.5659 38.0166C89.5659 39.3343 89.3511 40.4551 88.9214 41.3789C88.4917 42.3027 87.9009 43.0081 87.1489 43.4951C86.4041 43.9749 85.5591 44.2148 84.6138 44.2148C83.9334 44.2148 83.3641 44.1003 82.9058 43.8711C82.4546 43.6348 82.0894 43.3555 81.8101 43.0332C81.5379 42.7038 81.3302 42.3923 81.187 42.0986H81.0688V48.4902H78.2437ZM83.8403 41.8945C84.4634 41.8945 84.9826 41.7262 85.3979 41.3896C85.8205 41.0531 86.1392 40.5911 86.354 40.0039C86.5688 39.4167 86.6763 38.7507 86.6763 38.0059C86.6763 37.2611 86.5688 36.5986 86.354 36.0186C86.1392 35.4385 85.8241 34.9837 85.4087 34.6543C84.9933 34.3249 84.4705 34.1602 83.8403 34.1602C83.2316 34.1602 82.716 34.3177 82.2935 34.6328C81.8781 34.9479 81.5594 35.3955 81.3374 35.9756C81.1226 36.5485 81.0151 37.2253 81.0151 38.0059C81.0151 38.7793 81.1226 39.4596 81.3374 40.0469C81.5594 40.627 81.8817 41.0817 82.3042 41.4111C82.7267 41.7334 83.2388 41.8945 83.8403 41.8945Z" />
<path d="M70.042 44.2471C69.2757 44.2471 68.5882 44.111 67.9795 43.8389C67.3708 43.5596 66.891 43.1514 66.54 42.6143C66.1891 42.0771 66.0137 41.4147 66.0137 40.627C66.0137 39.9538 66.139 39.3952 66.3896 38.9512C66.6403 38.5072 66.9805 38.1527 67.4102 37.8877C67.847 37.6227 68.3376 37.4222 68.8818 37.2861C69.4261 37.1429 69.9883 37.0426 70.5684 36.9854C71.2773 36.9066 71.8503 36.8385 72.2871 36.7812C72.724 36.724 73.0391 36.6309 73.2324 36.502C73.4329 36.373 73.5332 36.1725 73.5332 35.9004V35.8467C73.5332 35.2666 73.3613 34.8154 73.0176 34.4932C72.6738 34.1709 72.1725 34.0098 71.5137 34.0098C70.819 34.0098 70.2676 34.1602 69.8594 34.4609C69.4583 34.7617 69.1862 35.1162 69.043 35.5244L66.4111 35.1484C66.6188 34.418 66.9626 33.8092 67.4424 33.3223C67.9222 32.8281 68.5059 32.4593 69.1934 32.2158C69.888 31.9652 70.6543 31.8398 71.4922 31.8398C72.0651 31.8398 72.638 31.9079 73.2109 32.0439C73.7839 32.1729 74.3102 32.3949 74.79 32.71C75.2699 33.0251 75.653 33.4476 75.9395 33.9775C76.2259 34.5075 76.3691 35.1699 76.3691 35.9648V44H73.6514V42.3457H73.5654C73.3936 42.6751 73.1501 42.9867 72.835 43.2803C72.527 43.5667 72.1403 43.7995 71.6748 43.9785C71.2165 44.1576 70.6722 44.2471 70.042 44.2471ZM70.7617 42.1631C71.3346 42.1631 71.8288 42.0521 72.2441 41.8301C72.6595 41.6009 72.9818 41.3001 73.2109 40.9277C73.4401 40.5482 73.5547 40.1364 73.5547 39.6924V38.2852C73.4616 38.3568 73.3112 38.4248 73.1035 38.4893C72.8958 38.5465 72.6595 38.6003 72.3945 38.6504C72.1296 38.7005 71.8682 38.7435 71.6104 38.7793C71.3597 38.8151 71.1413 38.8438 70.9551 38.8652C70.5326 38.9225 70.153 39.0192 69.8164 39.1553C69.487 39.2842 69.2256 39.4632 69.0322 39.6924C68.8389 39.9215 68.7422 40.2188 68.7422 40.584C68.7422 41.1068 68.932 41.5007 69.3115 41.7656C69.6911 42.0306 70.1745 42.1631 70.7617 42.1631Z" />
<path d="M53.7939 44V27.9941H59.7988C61.0306 27.9941 62.0618 28.2233 62.8926 28.6816C63.7305 29.14 64.3643 29.7702 64.7939 30.5723C65.2236 31.3743 65.4385 32.2839 65.4385 33.3008C65.4385 34.3249 65.2201 35.238 64.7832 36.04C64.3535 36.835 63.7161 37.4616 62.8711 37.9199C62.026 38.3711 60.9876 38.5967 59.7559 38.5967H55.7812V36.2119H59.3584C60.0817 36.2119 60.6725 36.0902 61.1309 35.8467C61.5964 35.596 61.9365 35.2523 62.1514 34.8154C62.3734 34.3714 62.4844 33.8665 62.4844 33.3008C62.4844 32.7279 62.3734 32.2266 62.1514 31.7969C61.9365 31.36 61.5964 31.0234 61.1309 30.7871C60.6725 30.5436 60.0781 30.4219 59.3477 30.4219H56.6943V44H53.7939Z" />
<path d="M46.2611 33.6556L34.7307 44.6408C32.7445 46.5331 29.5205 46.5323 27.5303 44.6361C25.5404 42.74 25.54 39.669 27.5261 37.7768L39.0565 26.7916C40.2474 25.657 42.1811 25.6567 43.3752 26.7943C44.5693 27.9319 44.5689 29.7742 43.378 30.9088L31.849 41.8954C31.4528 42.2728 30.8077 42.273 30.4092 41.8933C30.011 41.5136 30.0117 40.8996 30.4078 40.5223L41.9382 29.5371C42.336 29.1581 42.336 28.5437 41.9382 28.1647C41.5404 27.786 40.8954 27.7858 40.4977 28.1647L28.9673 39.1499C27.7742 40.2866 27.7764 42.1295 28.9687 43.2657C30.1613 44.402 32.0963 44.4046 33.2895 43.2678L44.8192 32.2805C46.8068 30.3865 46.8038 27.3159 44.8157 25.4219C42.8275 23.5277 39.6039 23.5253 37.616 25.4192L26.0856 36.4044C23.3024 39.056 23.3058 43.3558 26.0898 46.0085C28.8741 48.6612 33.388 48.6649 36.1712 46.0132L47.7017 35.028C48.0994 34.649 48.0994 34.0346 47.7017 33.6556C47.3039 33.2766 46.6589 33.2766 46.2611 33.6556Z" />
</svg>
);
}

View File

@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
import { AsciiArtAnimation } from "@/components/AsciiArtAnimation";
import { PaperclipLoading } from "@/components/AnimatedPaperclipIcon";
import { ThemeToggle } from "@/components/ThemeToggle";
import { Sparkles } from "lucide-react";
import { PaperclipLockup } from "../components/PaperclipLockup";
type AuthMode = "sign_in" | "sign_up";
@ -88,9 +88,8 @@ export function AuthPage() {
{/* Left half — form */}
<div className="w-full md:w-1/2 flex flex-col overflow-y-auto">
<div className="w-full max-w-md mx-auto my-auto px-8 py-12">
<div className="flex items-center gap-2 mb-8">
<Sparkles className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Paperclip</span>
<div className="mb-8">
<PaperclipLockup className="h-5 w-auto" />
</div>
<h1 className="text-xl font-semibold">