refactor(onboarding): put the workspace step on the shared frame

It was the only step still measuring at STEP_COLUMN, so its eyebrow, headline
and footer CTA started and ended ~150px inside where every other step's did —
the page margins visibly moved when you advanced from step 1 to step 2.

The step now sits on STEP_FRAME like the other two, with a new STEP_MEASURE
capping the prose and the form inside it. Matching the frame is not the same
as widening the field: a workspace name does not want an 800px input, so the
form keeps its reading measure and left-aligns to the frame instead. The
footer row spans the frame, which is what puts the CTA in the same place on
all three screens.

STEP_MEASURE deliberately does not centre — centring would pull the content
off the frame's left edge, undoing the alignment. Its test pins that.

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Lambda
2026-07-31 07:30:14 +08:00
parent fc94f60897
commit 8cc0d2e981
3 changed files with 28 additions and 8 deletions

View File

@@ -8,6 +8,7 @@ import {
STEP_COLUMN,
STEP_FRAME,
STEP_GUTTER,
STEP_MEASURE,
StepShellHeader,
} from "./step-shell";
@@ -56,6 +57,15 @@ describe("onboarding step shell", () => {
expect(STEP_BLOCK_PADDING).toMatch(/^py-/);
});
// STEP_MEASURE caps prose and form fields inside a step that sits on the
// frame. It must NOT centre: centring it would pull the content off the
// frame's left edge, which is the alignment the frame exists to provide.
it("keeps the in-frame reading measure left-aligned and padding-free", () => {
expect(STEP_MEASURE).toMatch(/max-w-\[\d+px\]/);
expect(STEP_MEASURE).not.toContain("mx-auto");
expect(STEP_MEASURE).not.toMatch(/\bp[xlr]?-/);
});
it("renders Back only when the step can go back", () => {
const { unmount } = renderHeader({ currentStep: "workspace" });
expect(screen.queryByRole("button", { name: /back/i })).toBeNull();

View File

@@ -22,15 +22,24 @@ import type { OnboardingStep } from "@multica/core/onboarding";
* - STEP_FRAME is the persistent chrome measure. Back and the step
* indicator sit on it, identically, on every step, so the one element
* that survives every transition never moves.
* - STEP_COLUMN is the reading measure for prose and forms. A step may use
* the frame instead when its content genuinely needs the width (the
* questionnaire's option grid does); both are centred, so the content
* stays on the frame's centreline either way.
* - STEP_COLUMN is the reading measure for prose and forms, centred. A step
* may use the frame instead when its content needs the width (the
* questionnaire's option grid, the runtime list) and cap its own prose
* with STEP_MEASURE; both are centred, so content stays on the frame's
* centreline either way.
*/
export const STEP_GUTTER = "px-6 sm:px-10 md:px-14 lg:px-16";
export const STEP_FRAME = "mx-auto w-full max-w-[920px]";
export const STEP_COLUMN = "mx-auto w-full max-w-[620px]";
/**
* Reading measure applied INSIDE the frame, left-aligned rather than centred.
* Use it for prose and form fields on a step that sits on STEP_FRAME: the
* page margins then match every other step, while a workspace name still gets
* an input someone would want to type into instead of an 800px one.
*/
export const STEP_MEASURE = "max-w-[620px]";
/** Vertical rhythm shared by every step's scrolling region. */
export const STEP_BLOCK_PADDING = "py-10";

View File

@@ -18,7 +18,8 @@ import { DragStrip } from "@multica/views/platform";
import { useLogout } from "../../auth";
import {
STEP_BLOCK_PADDING,
STEP_COLUMN,
STEP_FRAME,
STEP_MEASURE,
STEP_GUTTER,
StepShellHeader,
} from "../components/step-shell";
@@ -309,7 +310,7 @@ export function StepWorkspace({
style={fadeStyle}
className={cn("min-h-0 flex-1 overflow-y-auto", STEP_GUTTER)}
>
<div className={cn(STEP_COLUMN, STEP_BLOCK_PADDING)}>
<div className={cn(STEP_FRAME, STEP_BLOCK_PADDING)}>
<div className="mb-2 text-caption font-medium uppercase tracking-[0.08em] text-muted-foreground">
{reusing
? workspaceCreationAllowed
@@ -328,7 +329,7 @@ export function StepWorkspace({
? t(($) => $.step_workspace.headline_first)
: t(($) => $.step_workspace.creation_disabled_headline)}
</h1>
<p className="mt-4 text-body-lg leading-[1.55] text-foreground">
<p className={cn("mt-4 text-body-lg leading-[1.55] text-foreground", STEP_MEASURE)}>
{reusing
? workspaceCreationAllowed
? t(($) => $.step_workspace.lede_resume)
@@ -338,7 +339,7 @@ export function StepWorkspace({
: t(($) => $.step_workspace.creation_disabled_lede)}
</p>
<div className="mt-10">
<div className={cn("mt-10", STEP_MEASURE)}>
{reusing ? (
<div className="flex flex-col gap-3">
<ExistingWorkspaceCard