mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-04 17:18:35 +02:00
fix(onboarding): close the gap above the step CTA and put Log out back on one row
Two defects that read as one thing on screen: a large empty block sitting directly above the primary button. The card list was capped at STEP_MEASURE along with the form. That cap exists so a workspace name does not get an 800px input — a good reason for a text field and a bad one for selection cards, which are a list like the runtime grid. Capped, they stopped 299px short of the CTA, leaving a void above the button. The cards take the frame now; only the form keeps the reading measure. Log out came in from main as `fixed right-8 top-8`, pinned to the window corner. Its own comment says the fixed position exists to survive the flow's full-bleed layouts — which is what the measured frame replaced, so it landed outside the measure and above Back / Step N of N as a second header row. It now rides the header row on the frame. StepShellHeader takes it as a `trailing` slot rather than rendering it: calling useLogout inside the shared header forced a QueryClient into five step test files just to render a header bar. The flow injects it, matching how runtimeInstructions is already threaded, and the header stays presentational. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -2,16 +2,24 @@
|
||||
|
||||
import { LogOut } from "lucide-react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import { useLogout } from "../../auth";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
/**
|
||||
* Account-switch escape hatch shared by every onboarding step.
|
||||
*
|
||||
* The fixed position keeps it available across the flow's full-bleed layouts,
|
||||
* including the narrow mobile layout where the issue was originally reported.
|
||||
* `fixed` only for the welcome screen, which has no step header to sit in.
|
||||
* Every other step passes `inline` so it rides the header row on STEP_FRAME:
|
||||
* pinning it to the window corner put it outside the measure and above
|
||||
* Back / Step N of N, which read as a second header row.
|
||||
*/
|
||||
export function OnboardingLogoutButton() {
|
||||
export function OnboardingLogoutButton({
|
||||
inline = false,
|
||||
}: {
|
||||
/** Render in normal flow (inside a step header) instead of pinned. */
|
||||
inline?: boolean;
|
||||
} = {}) {
|
||||
const { t } = useT("onboarding");
|
||||
const logout = useLogout();
|
||||
|
||||
@@ -19,7 +27,10 @@ export function OnboardingLogoutButton() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="fixed right-8 top-8 z-50 text-muted-foreground hover:text-destructive"
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-destructive",
|
||||
inline ? "-mr-2 shrink-0" : "fixed right-8 top-8 z-50",
|
||||
)}
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
onClick={logout}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import { useT } from "../../i18n";
|
||||
@@ -52,11 +53,16 @@ export function StepShellHeader({
|
||||
currentStep,
|
||||
onBack,
|
||||
backDisabled,
|
||||
trailing,
|
||||
}: {
|
||||
currentStep: OnboardingStep;
|
||||
onBack?: () => void;
|
||||
/** Workspace step disables Back while its create request is in flight. */
|
||||
backDisabled?: boolean;
|
||||
/** Injected by the flow — the Log out escape hatch. A slot rather than a
|
||||
* direct render so this header stays presentational: rendering the logout
|
||||
* mutation inline forced a QueryClient into five step test files. */
|
||||
trailing?: ReactNode;
|
||||
}) {
|
||||
const { t } = useT("onboarding");
|
||||
return (
|
||||
@@ -78,6 +84,7 @@ export function StepShellHeader({
|
||||
<div className="flex-1">
|
||||
<StepHeader currentStep={currentStep} />
|
||||
</div>
|
||||
{trailing}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -125,12 +125,7 @@ interface OnboardingFlowProps {
|
||||
}
|
||||
|
||||
export function OnboardingFlow(props: OnboardingFlowProps) {
|
||||
return (
|
||||
<>
|
||||
<OnboardingLogoutButton />
|
||||
<OnboardingStepFlow {...props} />
|
||||
</>
|
||||
);
|
||||
return <OnboardingStepFlow {...props} />;
|
||||
}
|
||||
|
||||
function OnboardingStepFlow({
|
||||
@@ -333,6 +328,11 @@ function OnboardingStepFlow({
|
||||
// guidance. The remaining exits both end somewhere coherent: Skip runs the
|
||||
// runtime-skipped path (guide issue + land in the new workspace), and
|
||||
// continuing provisions Mika.
|
||||
// Log out lives in the step header row, on the frame, rather than pinned to
|
||||
// the window corner: pinned put it outside the measure and above Back /
|
||||
// Step N of N, which read as a second header row.
|
||||
const headerTrailing = <OnboardingLogoutButton inline />;
|
||||
|
||||
const runtimeStepBack = isNewWorkspace
|
||||
? undefined
|
||||
: () => handleBack("runtime");
|
||||
@@ -340,18 +340,23 @@ function OnboardingStepFlow({
|
||||
// Every step owns its full-bleed shell; this component only switches
|
||||
// between the active screen.
|
||||
if (step === "welcome") {
|
||||
// Welcome has no step header, so the escape hatch stays pinned here.
|
||||
return (
|
||||
<StepWelcome
|
||||
onNext={handleWelcomeNext}
|
||||
onSkip={canSkipWelcome ? handleWelcomeSkip : undefined}
|
||||
isWeb={isWeb}
|
||||
/>
|
||||
<>
|
||||
<OnboardingLogoutButton />
|
||||
<StepWelcome
|
||||
onNext={handleWelcomeNext}
|
||||
onSkip={canSkipWelcome ? handleWelcomeSkip : undefined}
|
||||
isWeb={isWeb}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === "about_you") {
|
||||
return (
|
||||
<StepAboutYou
|
||||
headerTrailing={headerTrailing}
|
||||
answers={answers}
|
||||
onChange={applyAnswers}
|
||||
onAdvance={() => advanceFrom("about_you")}
|
||||
@@ -364,6 +369,7 @@ function OnboardingStepFlow({
|
||||
if (step === "workspace") {
|
||||
return (
|
||||
<StepWorkspace
|
||||
headerTrailing={headerTrailing}
|
||||
existing={existingWorkspace}
|
||||
onCreated={handleWorkspaceCreated}
|
||||
onBack={() => handleBack("workspace")}
|
||||
@@ -381,6 +387,7 @@ function OnboardingStepFlow({
|
||||
if (!runtimeInstructions) {
|
||||
return (
|
||||
<StepRuntimeConnect
|
||||
headerTrailing={headerTrailing}
|
||||
wsId={workspace.id}
|
||||
wsSlug={workspace.slug}
|
||||
onNext={handleRuntimeNext}
|
||||
@@ -392,6 +399,7 @@ function OnboardingStepFlow({
|
||||
}
|
||||
return (
|
||||
<StepPlatformFork
|
||||
headerTrailing={headerTrailing}
|
||||
wsId={workspace.id}
|
||||
wsSlug={workspace.slug}
|
||||
onNext={handleRuntimeNext}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useRef , type ReactNode } from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Brain,
|
||||
@@ -64,12 +64,16 @@ export function StepAboutYou({
|
||||
onAdvance,
|
||||
onSkip,
|
||||
onBack,
|
||||
headerTrailing,
|
||||
}: {
|
||||
answers: QuestionnaireAnswers;
|
||||
onChange: (patch: Partial<QuestionnaireAnswers>) => void;
|
||||
onAdvance: () => void;
|
||||
onSkip: () => void;
|
||||
onBack?: () => void;
|
||||
/** Log out escape hatch, injected by the flow so this step does not depend
|
||||
* on the auth layer. */
|
||||
headerTrailing?: ReactNode;
|
||||
}) {
|
||||
const { t } = useT("onboarding");
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
@@ -185,7 +189,7 @@ export function StepAboutYou({
|
||||
return (
|
||||
<div className="animate-onboarding-enter flex h-full min-h-0 flex-col bg-background">
|
||||
<DragStrip />
|
||||
<StepShellHeader currentStep="about_you" onBack={onBack} />
|
||||
<StepShellHeader currentStep="about_you" onBack={onBack} trailing={headerTrailing} />
|
||||
|
||||
<main
|
||||
ref={mainRef}
|
||||
|
||||
@@ -60,6 +60,7 @@ export function StepPlatformFork({
|
||||
wsSlug,
|
||||
onNext,
|
||||
onBack,
|
||||
headerTrailing,
|
||||
cliInstructions,
|
||||
}: {
|
||||
wsId: string;
|
||||
@@ -69,6 +70,9 @@ export function StepPlatformFork({
|
||||
wsSlug?: string;
|
||||
onNext: (runtime: AgentRuntime | null) => void | Promise<void>;
|
||||
onBack?: () => void;
|
||||
/** Log out escape hatch, injected by the flow so this step does not depend
|
||||
* on the auth layer. */
|
||||
headerTrailing?: ReactNode;
|
||||
/** Platform-specific CLI install card, rendered inside the CLI dialog. */
|
||||
cliInstructions?: ReactNode;
|
||||
}) {
|
||||
@@ -113,7 +117,7 @@ export function StepPlatformFork({
|
||||
<div className="animate-onboarding-enter flex h-full min-h-0 flex-col bg-background">
|
||||
<DragStrip />
|
||||
|
||||
<StepShellHeader currentStep="runtime" onBack={onBack} />
|
||||
<StepShellHeader currentStep="runtime" onBack={onBack} trailing={headerTrailing} />
|
||||
|
||||
<main
|
||||
ref={mainRef}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState , type ReactNode } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowRight,
|
||||
@@ -48,6 +48,7 @@ export function StepRuntimeConnect({
|
||||
wsSlug,
|
||||
onNext,
|
||||
onBack,
|
||||
headerTrailing,
|
||||
onRefresh,
|
||||
runtimesPending,
|
||||
}: {
|
||||
@@ -58,6 +59,9 @@ export function StepRuntimeConnect({
|
||||
wsSlug?: string;
|
||||
onNext: (runtime: AgentRuntime | null) => void | Promise<void>;
|
||||
onBack?: () => void;
|
||||
/** Log out escape hatch, injected by the flow so this step does not depend
|
||||
* on the auth layer. */
|
||||
headerTrailing?: ReactNode;
|
||||
/** Platform-level rescan hook. Desktop wires this to restart the
|
||||
* bundled daemon so a freshly-installed CLI shows up — otherwise the
|
||||
* daemon's PATH probe runs once at boot and never re-probes. */
|
||||
@@ -82,6 +86,7 @@ export function StepRuntimeConnect({
|
||||
setSelectedId={setSelectedId}
|
||||
onNext={onNext}
|
||||
onBack={onBack}
|
||||
headerTrailing={headerTrailing}
|
||||
onRefresh={onRefresh}
|
||||
runtimesPending={runtimesPending}
|
||||
/>
|
||||
@@ -111,6 +116,7 @@ function FancyView({
|
||||
setSelectedId,
|
||||
onNext,
|
||||
onBack,
|
||||
headerTrailing,
|
||||
onRefresh,
|
||||
runtimesPending,
|
||||
}: {
|
||||
@@ -121,6 +127,7 @@ function FancyView({
|
||||
setSelectedId: (id: string) => void;
|
||||
onNext: (runtime: AgentRuntime | null) => void | Promise<void>;
|
||||
onBack?: () => void;
|
||||
headerTrailing?: ReactNode;
|
||||
onRefresh?: () => void | Promise<void>;
|
||||
runtimesPending?: boolean;
|
||||
}) {
|
||||
@@ -231,7 +238,7 @@ function FancyView({
|
||||
<DragStrip />
|
||||
|
||||
{/* Header — Back + horizontal step indicator */}
|
||||
<StepShellHeader currentStep="runtime" onBack={onBack} />
|
||||
<StepShellHeader currentStep="runtime" onBack={onBack} trailing={headerTrailing} />
|
||||
|
||||
{/* Scrollable middle — content changes by phase but always wraps
|
||||
at STEP_FRAME — the same measure as the header — so nine
|
||||
|
||||
@@ -71,10 +71,14 @@ export function StepWorkspace({
|
||||
existing,
|
||||
onCreated,
|
||||
onBack,
|
||||
headerTrailing,
|
||||
}: {
|
||||
existing?: Workspace | null;
|
||||
onCreated: (workspace: Workspace) => void | Promise<void>;
|
||||
onBack?: () => void;
|
||||
/** Log out escape hatch, injected by the flow so this step does not depend
|
||||
* on the auth layer. */
|
||||
headerTrailing?: ReactNode;
|
||||
}) {
|
||||
const { t, i18n } = useT("onboarding");
|
||||
const locale = matchLocale([i18n.resolvedLanguage ?? i18n.language]);
|
||||
@@ -221,7 +225,7 @@ export function StepWorkspace({
|
||||
}
|
||||
|
||||
const createFields = (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className={cn("flex flex-col gap-5", STEP_MEASURE)}>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label
|
||||
htmlFor="ws-name"
|
||||
@@ -303,6 +307,7 @@ export function StepWorkspace({
|
||||
currentStep="workspace"
|
||||
onBack={onBack}
|
||||
backDisabled={isCreating}
|
||||
trailing={headerTrailing}
|
||||
/>
|
||||
|
||||
<main
|
||||
@@ -339,7 +344,7 @@ export function StepWorkspace({
|
||||
: t(($) => $.step_workspace.creation_disabled_lede)}
|
||||
</p>
|
||||
|
||||
<div className={cn("mt-10", STEP_MEASURE)}>
|
||||
<div className="mt-10">
|
||||
{reusing ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
<ExistingWorkspaceCard
|
||||
|
||||
Reference in New Issue
Block a user