Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 30 additions & 68 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1947,22 +1947,21 @@ select:disabled,
question per phase. Deliberately does not reuse `--radius` — this chrome
commits to square corners everywhere, the rest of the app does not. */
.onboarding-shell {
display: grid;
min-height: 100svh;
background: var(--background);
}

.onboarding-form-col {
position: relative;
display: flex;
flex-direction: column;
min-height: 100svh;
}

.onboarding-brand {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 2rem;
padding: 1.5rem 2rem;
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.025em;
Expand All @@ -1974,8 +1973,8 @@ select:disabled,
width: 1.75rem;
flex-shrink: 0;
place-items: center;
border-radius: 0;
background: var(--shell-accent);
border-radius: var(--radius);
background: var(--primary);
color: var(--primary-foreground);
}

Expand All @@ -1985,59 +1984,54 @@ select:disabled,
}

.onboarding-form-slot {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding: 1rem 2rem 4rem;
padding: 4vh 1.5rem 4rem;
}

.onboarding-form {
width: 100%;
max-width: 30rem;
max-width: 35rem;
margin: 0 auto;
}

.onboarding-panel {
position: relative;
display: none;
overflow: hidden;
background: var(--foreground);
/* Step progress: charcoal for done, ink-on-line for now, muted for ahead. */
.onboarding-progress {
display: flex;
gap: 6px;
margin: 0 0 1.75rem;
padding: 0;
list-style: none;
}

.dark .onboarding-panel {
background: var(--card);
.onboarding-progress li {
flex: 1;
height: 4px;
border-radius: 2px;
background: var(--border);
}

.onboarding-panel-fallback {
position: absolute;
inset: 0;
height: 100%;
width: 100%;
object-fit: cover;
.onboarding-progress li[data-state="done"] {
background: var(--foreground);
}

.onboarding-panel-dither {
position: absolute;
inset: 0;
.onboarding-progress li[data-state="now"] {
background: var(--shell-accent);
}

@media (min-width: 64rem) {
.onboarding-shell {
grid-template-columns: 1fr 1fr;
}

.onboarding-panel {
display: block;
}
.onboarding-actions {
display: flex;
align-items: center;
gap: 0.625rem;
margin-top: 1.25rem;
}

/* One focused question per phase, large type, no card chrome around it —
the whitespace of the form column does the framing. */
.onboarding-title {
margin: 0 0 0.75rem;
font-size: 2rem;
font-size: 1.875rem;
line-height: 1.15;
font-weight: 600;
font-weight: 800;
letter-spacing: -0.02em;
}

Expand Down Expand Up @@ -2072,32 +2066,6 @@ select:disabled,
gap: 0.875rem;
}

/* Square corners throughout the wizard's own inputs and buttons — the one
deliberate departure from the app's default `--radius`. */
.onboarding-content input,
.onboarding-content textarea,
.onboarding-content button,
.onboarding-content [role="radiogroup"] button {
border-radius: 0;
}

/* The escape hatch below the form: low-emphasis on purpose, since
connecting a provider is still the primary path — this is only for the
account that genuinely has nothing to paste yet. */
.onboarding-credential-skip {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
margin-top: -0.25rem;
}

.onboarding-credential-skip-hint {
margin: 0;
font-size: 0.8125rem;
color: var(--muted-foreground);
}

/* The one-click paths sit above the key forms, side by side in a
two-column row so both fit without stacking the page tall — each card
stays square-cornered like the rest of the wizard, with the shell
Expand Down Expand Up @@ -2179,12 +2147,6 @@ select:disabled,
color: var(--muted-foreground);
}

.onboarding-name-form button[type="submit"],
.onboarding-credential-form button[type="submit"] {
margin-top: 0.5rem;
align-self: flex-start;
}

.onboarding-provider-picker {
display: flex;
flex-wrap: wrap;
Expand Down
36 changes: 23 additions & 13 deletions apps/web/src/onboarding/onboarding-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { CorbitsMark } from "@corbits/react-ui";
import type { ReactNode } from "react";

import { DitherBackground } from "../auth/dither-background";
const STEP_LABELS = ["Workspace", "Model", "Myra"] as const;

/**
* Full-screen wizard frame, modeled on `AuthLayout`'s two-column shell for
* visual continuity with the sign-in screen: a form column on the left and
* a dithered brand panel on the right (hidden on small screens). Wider than
* the auth form column — the credential step's provider picker and the
* guidance cards need more space than a login form does.
* Single centered column with a step progress bar, matching the onboarding
* mockup. `step` is the index of the current step (0-2).
*/
export function OnboardingLayout({ children }: { readonly children: ReactNode }) {
export function OnboardingLayout({
step,
children,
}: {
readonly step: 0 | 1 | 2;
readonly children: ReactNode;
}) {
return (
<div className="onboarding-shell">
<main className="onboarding-form-col">
Expand All @@ -22,14 +25,21 @@ export function OnboardingLayout({ children }: { readonly children: ReactNode })
</div>

<div className="onboarding-form-slot">
<div className="onboarding-form">{children}</div>
<div className="onboarding-form">
<ol className="onboarding-progress" aria-label="Setup progress">
{STEP_LABELS.map((label, index) => (
<li
key={label}
data-state={index < step ? "done" : index === step ? "now" : "ahead"}
aria-label={label}
aria-current={index === step ? "step" : undefined}
/>
))}
</ol>
{children}
</div>
</div>
</main>

<div aria-hidden className="onboarding-panel">
<img src="/images/hero-dither.png" alt="" className="onboarding-panel-fallback" />
<DitherBackground className="onboarding-panel-dither" />
</div>
</div>
);
}
13 changes: 10 additions & 3 deletions apps/web/src/onboarding/provider-connect-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,12 @@ export function ProviderConnectStep({
tenantId,
onConnected,
onError,
onSkip,
}: {
readonly tenantId: string;
readonly onConnected: (offering: ExistingOffering) => void;
readonly onError: (message: string) => void;
readonly onSkip: () => void;
}) {
const [selected, setSelected] = useState<string | null>(null);
const [apiKey, setApiKey] = useState("");
Expand Down Expand Up @@ -417,9 +419,14 @@ export function ProviderConnectStep({
/>
</label>
)}
<Button type="submit" disabled={submitting || waiting || !ready}>
{submitLabel}
</Button>
<div className="onboarding-actions">
<Button type="submit" variant="primary" disabled={submitting || waiting || !ready}>
{submitLabel}
</Button>
<Button type="button" variant="ghost" onClick={onSkip}>
Skip for now
</Button>
</div>
</form>
);
}
38 changes: 25 additions & 13 deletions apps/web/src/pages/onboarding-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ensurePrimaryTenant, runPortableClientBootstrap } from "../client-boots
import { createFetchStockHub, findOwnedTenants } from "../needs-converge";
import { deployMyraSource } from "../myra-deploy";
import { useNavigate } from "../navigation";
import { NEW_WORKBENCH_PATH } from "../routes";
import { triggerFirstLoginProvisioning } from "../onboarding";
import { OnboardingLayout } from "../onboarding/onboarding-layout";
import {
Expand Down Expand Up @@ -184,7 +185,7 @@ export function OnboardingPage({ user }: { readonly user: SessionUser }) {

if (state.phase === "checking" || state.phase === "resolving-tenant") {
return (
<OnboardingLayout>
<OnboardingLayout step={0}>
<div className="onboarding-phase onboarding-phase--loading" key="checking">
<WorkbenchLoadingState
delayMs={0}
Expand All @@ -199,11 +200,11 @@ export function OnboardingPage({ user }: { readonly user: SessionUser }) {

if (state.phase === "provider-setup") {
return (
<OnboardingLayout>
<OnboardingLayout step={1}>
<div className="onboarding-phase onboarding-phase--credential" key="provider-setup">
<h1 className="onboarding-title">Connect a model provider</h1>
<h1 className="onboarding-title">Connect a brain</h1>
<p className="onboarding-subtitle">
Myra needs one working inference credential before she can start.
Pick a provider and connect it, or skip for now and connect one later in Settings.
</p>
<div className="onboarding-content">
<ProviderConnectStep
Expand All @@ -227,6 +228,7 @@ export function OnboardingPage({ user }: { readonly user: SessionUser }) {
});
}}
onError={(message) => setState({ phase: "error", message })}
onSkip={() => navigate(NEW_WORKBENCH_PATH)}
/>
</div>
</div>
Expand All @@ -236,7 +238,7 @@ export function OnboardingPage({ user }: { readonly user: SessionUser }) {

if (state.phase === "publishing-myra" || state.phase === "installing") {
return (
<OnboardingLayout>
<OnboardingLayout step={2}>
<div className="onboarding-phase onboarding-phase--loading" key="installing">
<WorkbenchLoadingState
delayMs={0}
Expand All @@ -251,22 +253,27 @@ export function OnboardingPage({ user }: { readonly user: SessionUser }) {

if (state.phase === "setup-pending") {
return (
<OnboardingLayout>
<OnboardingLayout step={2}>
<div className="onboarding-phase" key="setup-pending">
<h1 className="onboarding-title">Set up your workbench</h1>
<p className="onboarding-subtitle">{state.message}</p>
<div className="onboarding-content">
<Button variant="outline" onClick={checkStatus}>
Check again
</Button>
<div className="onboarding-actions">
<Button variant="primary" onClick={checkStatus}>
Check again
</Button>
<Button variant="ghost" onClick={() => navigate(NEW_WORKBENCH_PATH)}>
Skip for now
</Button>
</div>
</div>
</div>
</OnboardingLayout>
);
}

return (
<OnboardingLayout>
<OnboardingLayout step={0}>
<div className="onboarding-phase" key="status-error">
<div className="onboarding-content">
<EmptyState
Expand All @@ -286,9 +293,14 @@ export function OnboardingPage({ user }: { readonly user: SessionUser }) {
)
}
action={
<Button variant="outline" onClick={checkStatus}>
Try again
</Button>
<div className="onboarding-actions">
<Button variant="primary" onClick={checkStatus}>
Try again
</Button>
<Button variant="ghost" onClick={() => navigate(NEW_WORKBENCH_PATH)}>
Skip for now
</Button>
</div>
}
/>
</div>
Expand Down
Loading