feat(FN-1943): add post-onboarding setup recommendations panel

- Add a PostOnboardingRecommendations component that detects incomplete AI provider, default model, and GitHub setup and offers direct actions
- Persist post-onboarding dismissal state in model-onboarding-state with new helpers for dismiss, check, and clear flows
- Integrate recommendations into App rendering so they appear after completed onboarding when resume onboarding is not active
- Add dashboard styles and focused tests for recommendation rendering/actions plus onboarding dismissal state coverage
- Add a changeset for @gsxdsm/fusion patch release describing the post-onboarding recommendations
This commit is contained in:
Fusion
2026-04-17 05:36:02 -07:00
committed by gsxdsm
parent 77531e8a97
commit 5f8597ea08
7 changed files with 711 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"@gsxdsm/fusion": patch
---
Add a post-onboarding recommendations panel that surfaces skipped setup steps and deep links users directly to complete AI provider, default model, and GitHub configuration.

View File

@@ -19,7 +19,13 @@ import { DashboardLoader, type DashboardLoaderStage } from "./components/Dashboa
import { ExecutorStatusBar } from "./components/ExecutorStatusBar";
import { SessionNotificationBanner } from "./components/SessionNotificationBanner";
import { OnboardingResumeCard } from "./components/OnboardingResumeCard";
import { isOnboardingResumable } from "./components/model-onboarding-state";
import { PostOnboardingRecommendations } from "./components/PostOnboardingRecommendations";
import {
isOnboardingCompleted,
isOnboardingResumable,
isPostOnboardingDismissed,
} from "./components/model-onboarding-state";
import type { SectionId } from "./components/SettingsModal";
import { MobileNavBar } from "./components/MobileNavBar";
import { QuickChatFAB } from "./components/QuickChatFAB";
import { ToastContainer } from "./components/ToastContainer";
@@ -503,6 +509,13 @@ function AppInner() {
);
}
const showOnboardingResumeCard = !modalManager.modelOnboardingOpen && isOnboardingResumable();
const showPostOnboardingRecommendations =
!modalManager.modelOnboardingOpen &&
!showOnboardingResumeCard &&
isOnboardingCompleted() &&
!isPostOnboardingDismissed();
return (
<>
<Header
@@ -559,9 +572,15 @@ function AppInner() {
onDismissAll={handleDismissAllNeedingInputSessions}
/>
)}
{viewMode === "project" && currentProject && !modalManager.modelOnboardingOpen && isOnboardingResumable() && (
{viewMode === "project" && currentProject && showOnboardingResumeCard && (
<OnboardingResumeCard onResume={modalManager.openModelOnboarding} />
)}
{viewMode === "project" && currentProject && showPostOnboardingRecommendations && (
<PostOnboardingRecommendations
onOpenModelOnboarding={modalManager.openModelOnboarding}
onOpenSettings={(section) => modalManager.openSettings(section as SectionId)}
/>
)}
<div
className={`project-content${viewMode === "project" && currentProject ? " project-content--with-footer" : ""}${isMobile ? " project-content--with-mobile-nav" : ""}`}
>

View File

@@ -0,0 +1,210 @@
import { useCallback, useEffect, useMemo, useState, type ComponentType } from "react";
import { AlertCircle, GitPullRequest, Key, Lightbulb, X, Zap } from "lucide-react";
import { fetchAuthStatus, fetchGlobalSettings } from "../api";
import {
dismissPostOnboardingRecommendations,
isOnboardingCompleted,
isPostOnboardingDismissed,
} from "./model-onboarding-state";
interface PostOnboardingRecommendationsProps {
onOpenSettings: (section: string) => void;
onOpenModelOnboarding: () => void;
}
interface RecommendationItem {
id: "ai-provider" | "default-model" | "github";
title: string;
description: string;
actionLabel: string;
onAction: () => void;
icon: ComponentType<{ size?: number; className?: string; "aria-hidden"?: boolean }>;
}
export function PostOnboardingRecommendations({
onOpenSettings,
onOpenModelOnboarding,
}: PostOnboardingRecommendationsProps) {
const onboardingCompleted = isOnboardingCompleted();
const postOnboardingDismissed = isPostOnboardingDismissed();
const [loading, setLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [dismissedLocally, setDismissedLocally] = useState(false);
const [incompleteState, setIncompleteState] = useState<{
needsAiProvider: boolean;
needsDefaultModel: boolean;
needsGitHub: boolean;
}>({
needsAiProvider: false,
needsDefaultModel: false,
needsGitHub: false,
});
useEffect(() => {
if (!onboardingCompleted || postOnboardingDismissed) {
setLoading(false);
return;
}
let cancelled = false;
const load = async () => {
try {
setLoading(true);
setHasError(false);
const [authStatus, globalSettings] = await Promise.all([
fetchAuthStatus(),
fetchGlobalSettings(),
]);
if (cancelled) {
return;
}
const providers = authStatus.providers ?? [];
const githubProvider = providers.find((provider) => provider.id === "github");
const hasAuthenticatedAiProvider = providers.some(
(provider) => provider.id !== "github" && provider.authenticated,
);
const needsAiProvider = !hasAuthenticatedAiProvider;
const needsDefaultModel = !globalSettings.defaultProvider && !globalSettings.defaultModelId;
const needsGitHub = githubProvider ? !githubProvider.authenticated : false;
setIncompleteState({ needsAiProvider, needsDefaultModel, needsGitHub });
} catch {
if (!cancelled) {
setHasError(true);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
void load();
return () => {
cancelled = true;
};
}, [onboardingCompleted, postOnboardingDismissed]);
const handleDismiss = useCallback(() => {
dismissPostOnboardingRecommendations();
setDismissedLocally(true);
}, []);
const handleOpenModelOnboarding = useCallback(() => {
onOpenModelOnboarding();
}, [onOpenModelOnboarding]);
const handleOpenGlobalModels = useCallback(() => {
onOpenSettings("global-models");
}, [onOpenSettings]);
const handleOpenAuthentication = useCallback(() => {
onOpenSettings("authentication");
}, [onOpenSettings]);
const recommendations = useMemo<RecommendationItem[]>(() => {
const items: RecommendationItem[] = [];
if (incompleteState.needsAiProvider) {
items.push({
id: "ai-provider",
title: "Connect AI Provider",
description: "Connect an AI provider to enable AI agents for task planning and code generation",
actionLabel: "Set Up AI",
onAction: handleOpenModelOnboarding,
icon: Zap,
});
}
if (incompleteState.needsDefaultModel) {
items.push({
id: "default-model",
title: "Select Default Model",
description: "Choose a default AI model for task execution",
actionLabel: "Choose Model",
onAction: handleOpenGlobalModels,
icon: Key,
});
}
if (incompleteState.needsGitHub) {
items.push({
id: "github",
title: "Connect GitHub",
description: "Connect GitHub to import issues and track pull requests",
actionLabel: "Connect GitHub",
onAction: handleOpenAuthentication,
icon: GitPullRequest,
});
}
return items;
}, [
incompleteState.needsAiProvider,
incompleteState.needsDefaultModel,
incompleteState.needsGitHub,
handleOpenAuthentication,
handleOpenGlobalModels,
handleOpenModelOnboarding,
]);
if (!onboardingCompleted || postOnboardingDismissed || dismissedLocally || loading || hasError || recommendations.length === 0) {
return null;
}
return (
<section
className="post-onboarding-recommendations"
role="region"
aria-label="Setup recommendations"
>
<div className="post-onboarding-recommendations__main">
<div className="post-onboarding-recommendations__icon" aria-hidden="true">
<Lightbulb size={18} aria-hidden={true} />
</div>
<div className="post-onboarding-recommendations__content">
<h2 className="post-onboarding-recommendations__title">Recommended Next Steps</h2>
<p className="post-onboarding-recommendations__description">
Complete these setup items to get the most out of Fusion.
</p>
<ul className="post-onboarding-recommendations__list">
{recommendations.map((item) => {
const ItemIcon = item.icon;
return (
<li key={item.id} className="post-onboarding-recommendations__item">
<span className="post-onboarding-recommendations__item-icon" aria-hidden="true">
<AlertCircle size={14} aria-hidden={true} />
<ItemIcon size={14} aria-hidden={true} />
</span>
<span className="post-onboarding-recommendations__item-text">
<strong>{item.title}</strong>
<span>{item.description}</span>
</span>
<button type="button" className="btn btn-sm btn-primary" onClick={item.onAction}>
{item.actionLabel}
</button>
</li>
);
})}
</ul>
</div>
</div>
<button
type="button"
className="post-onboarding-recommendations__dismiss"
onClick={handleDismiss}
aria-label="Dismiss recommendations"
>
<X size={16} aria-hidden={true} />
</button>
</section>
);
}

View File

@@ -0,0 +1,220 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { PostOnboardingRecommendations } from "../PostOnboardingRecommendations";
const mockFetchAuthStatus = vi.fn();
const mockFetchGlobalSettings = vi.fn();
vi.mock("../../api", () => ({
fetchAuthStatus: (...args: unknown[]) => mockFetchAuthStatus(...args),
fetchGlobalSettings: (...args: unknown[]) => mockFetchGlobalSettings(...args),
}));
const mockIsOnboardingCompleted = vi.fn();
const mockIsPostOnboardingDismissed = vi.fn();
const mockDismissPostOnboardingRecommendations = vi.fn();
vi.mock("../model-onboarding-state", () => ({
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
}));
vi.mock("lucide-react", async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
return {
...actual,
X: () => <span data-testid="icon-x">X</span>,
Lightbulb: () => <span data-testid="icon-lightbulb">Lightbulb</span>,
CheckCircle: () => <span data-testid="icon-check-circle">CheckCircle</span>,
AlertCircle: () => <span data-testid="icon-alert-circle">AlertCircle</span>,
Key: () => <span data-testid="icon-key">Key</span>,
GitPullRequest: () => <span data-testid="icon-git-pull-request">GitPullRequest</span>,
Zap: () => <span data-testid="icon-zap">Zap</span>,
};
});
describe("PostOnboardingRecommendations", () => {
const onOpenSettings = vi.fn();
const onOpenModelOnboarding = vi.fn();
const renderComponent = () => render(
<PostOnboardingRecommendations
onOpenSettings={onOpenSettings}
onOpenModelOnboarding={onOpenModelOnboarding}
/>,
);
beforeEach(() => {
vi.clearAllMocks();
mockIsOnboardingCompleted.mockReturnValue(true);
mockIsPostOnboardingDismissed.mockReturnValue(false);
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: true },
{ id: "github", name: "GitHub", authenticated: true },
],
});
mockFetchGlobalSettings.mockResolvedValue({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
});
});
it("renders nothing when onboarding is not completed", () => {
mockIsOnboardingCompleted.mockReturnValue(false);
const { container } = renderComponent();
expect(container.firstChild).toBeNull();
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
expect(mockFetchGlobalSettings).not.toHaveBeenCalled();
});
it("renders nothing when post-onboarding is dismissed", () => {
mockIsPostOnboardingDismissed.mockReturnValue(true);
const { container } = renderComponent();
expect(container.firstChild).toBeNull();
expect(mockFetchAuthStatus).not.toHaveBeenCalled();
expect(mockFetchGlobalSettings).not.toHaveBeenCalled();
});
it("renders nothing when all setup items are complete", async () => {
const { container } = renderComponent();
await waitFor(() => {
expect(mockFetchAuthStatus).toHaveBeenCalledTimes(1);
expect(mockFetchGlobalSettings).toHaveBeenCalledTimes(1);
});
expect(container.firstChild).toBeNull();
});
it("renders Connect AI Provider recommendation when no AI provider is authenticated", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: true },
],
});
renderComponent();
expect(await screen.findByText("Connect AI Provider")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Set Up AI" })).toBeInTheDocument();
});
it("renders Select Default Model recommendation when no default model is set", async () => {
mockFetchGlobalSettings.mockResolvedValue({});
renderComponent();
expect(await screen.findByText("Select Default Model")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Choose Model" })).toBeInTheDocument();
});
it("renders Connect GitHub recommendation when GitHub exists but is not authenticated", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: true },
{ id: "github", name: "GitHub", authenticated: false },
],
});
renderComponent();
expect(await screen.findByText("Connect GitHub to import issues and track pull requests")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Connect GitHub" })).toBeInTheDocument();
});
it("renders multiple recommendations when multiple setup items are incomplete", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: false },
],
});
mockFetchGlobalSettings.mockResolvedValue({});
renderComponent();
expect(await screen.findByText("Connect AI Provider")).toBeInTheDocument();
expect(screen.getByText("Select Default Model")).toBeInTheDocument();
expect(screen.getAllByText("Connect GitHub").length).toBeGreaterThan(0);
});
it("clicking Set Up AI calls onOpenModelOnboarding", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: true },
],
});
renderComponent();
const button = await screen.findByRole("button", { name: "Set Up AI" });
fireEvent.click(button);
expect(onOpenModelOnboarding).toHaveBeenCalledTimes(1);
});
it("clicking Choose Model calls onOpenSettings with global-models", async () => {
mockFetchGlobalSettings.mockResolvedValue({});
renderComponent();
const button = await screen.findByRole("button", { name: "Choose Model" });
fireEvent.click(button);
expect(onOpenSettings).toHaveBeenCalledWith("global-models");
});
it("clicking Connect GitHub calls onOpenSettings with authentication", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: true },
{ id: "github", name: "GitHub", authenticated: false },
],
});
renderComponent();
const button = await screen.findByRole("button", { name: "Connect GitHub" });
fireEvent.click(button);
expect(onOpenSettings).toHaveBeenCalledWith("authentication");
});
it("clicking dismiss calls dismissPostOnboardingRecommendations", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [
{ id: "anthropic", name: "Anthropic", authenticated: false },
{ id: "github", name: "GitHub", authenticated: true },
],
});
renderComponent();
const dismissButton = await screen.findByRole("button", { name: "Dismiss recommendations" });
fireEvent.click(dismissButton);
expect(mockDismissPostOnboardingRecommendations).toHaveBeenCalledTimes(1);
});
it("returns null on API error", async () => {
mockFetchAuthStatus.mockRejectedValue(new Error("network failure"));
const { container } = renderComponent();
await waitFor(() => {
expect(mockFetchAuthStatus).toHaveBeenCalledTimes(1);
});
expect(container.firstChild).toBeNull();
});
});

View File

@@ -3,6 +3,9 @@ import {
getOnboardingState,
saveOnboardingState,
clearOnboardingState,
dismissPostOnboardingRecommendations,
isPostOnboardingDismissed,
clearPostOnboardingDismissal,
isOnboardingResumable,
getOnboardingResumeStep,
markOnboardingCompleted,
@@ -357,6 +360,37 @@ describe("model-onboarding-state", () => {
});
});
describe("post-onboarding dismissal", () => {
it("dismissPostOnboardingRecommendations sets postOnboardingDismissedAt", () => {
saveOnboardingState("complete", { completed: true });
dismissPostOnboardingRecommendations();
const stored = mockStore[STORAGE_KEY];
const parsed = JSON.parse(stored);
expect(parsed.postOnboardingDismissedAt).toBeDefined();
});
it("isPostOnboardingDismissed returns true after dismissal", () => {
dismissPostOnboardingRecommendations();
expect(isPostOnboardingDismissed()).toBe(true);
});
it("isPostOnboardingDismissed returns false when not dismissed", () => {
saveOnboardingState("complete", { completed: true });
expect(isPostOnboardingDismissed()).toBe(false);
});
it("clearPostOnboardingDismissal removes the dismissal field", () => {
dismissPostOnboardingRecommendations();
clearPostOnboardingDismissal();
const state = getOnboardingState();
expect(state?.postOnboardingDismissedAt).toBeUndefined();
expect(isPostOnboardingDismissed()).toBe(false);
});
});
describe("isOnboardingCompleted", () => {
it("returns false when no state exists", () => {
expect(isOnboardingCompleted()).toBe(false);

View File

@@ -22,6 +22,8 @@ interface OnboardingState {
stepData: Partial<Record<OnboardingStep, Record<string, unknown>>>;
/** Legacy field: ISO-8601 timestamp when onboarding was marked complete */
completedAt?: string;
/** ISO-8601 timestamp when post-onboarding recommendations were dismissed */
postOnboardingDismissedAt?: string;
}
const STORAGE_KEY = "fusion_model_onboarding_state";
@@ -34,6 +36,7 @@ const DEFAULT_SKIPPED_STEPS: OnboardingStep[] = [];
const DEFAULT_DISMISSED = false;
const DEFAULT_COMPLETED = false;
const DEFAULT_STEP_DATA: Partial<Record<OnboardingStep, Record<string, unknown>>> = {};
const DEFAULT_POST_ONBOARDING_DISMISSED_AT: string | undefined = undefined;
/**
* Step labels for display in the resume card.
@@ -79,6 +82,8 @@ export function getOnboardingState(): OnboardingState | null {
* Apply default values for backward compatibility with partial/legacy state objects.
*/
function applyStateDefaults(state: OnboardingState): OnboardingState {
const postOnboardingDismissedAt = state.postOnboardingDismissedAt ?? DEFAULT_POST_ONBOARDING_DISMISSED_AT;
return {
...state,
completedSteps: state.completedSteps ?? DEFAULT_COMPLETED_STEPS,
@@ -86,6 +91,7 @@ function applyStateDefaults(state: OnboardingState): OnboardingState {
dismissed: state.dismissed ?? DEFAULT_DISMISSED,
completed: state.completed ?? DEFAULT_COMPLETED,
stepData: state.stepData ?? DEFAULT_STEP_DATA,
...(postOnboardingDismissedAt ? { postOnboardingDismissedAt } : {}),
};
}
@@ -371,6 +377,71 @@ export function isOnboardingCompleted(): boolean {
return typeof state.completedAt === "string" && state.completedAt.length > 0;
}
/**
* Mark the post-onboarding recommendations as dismissed.
*/
export function dismissPostOnboardingRecommendations(): void {
if (typeof window === "undefined") return;
try {
const existing = getOnboardingState();
const now = new Date().toISOString();
const state: OnboardingState = existing
? {
...existing,
updatedAt: now,
postOnboardingDismissedAt: now,
}
: {
currentStep: "complete",
updatedAt: now,
completedSteps: DEFAULT_COMPLETED_STEPS,
skippedSteps: DEFAULT_SKIPPED_STEPS,
dismissed: false,
completed: false,
stepData: DEFAULT_STEP_DATA,
postOnboardingDismissedAt: now,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch {
// Storage quota exceeded or private browsing - fail silently
}
}
/**
* Whether post-onboarding recommendations were dismissed.
*/
export function isPostOnboardingDismissed(): boolean {
const state = getOnboardingState();
if (!state) return false;
return typeof state.postOnboardingDismissedAt === "string" && state.postOnboardingDismissedAt.length > 0;
}
/**
* Remove persisted post-onboarding dismissal state.
*/
export function clearPostOnboardingDismissal(): void {
if (typeof window === "undefined") return;
try {
const existing = getOnboardingState();
if (!existing) return;
const { postOnboardingDismissedAt: _postOnboardingDismissedAt, ...stateWithoutDismissal } = existing;
const state: OnboardingState = {
...stateWithoutDismissal,
updatedAt: new Date().toISOString(),
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch {
// Storage quota exceeded or private browsing - fail silently
}
}
/**
* Get the ISO-8601 timestamp when onboarding was marked complete.
* Returns null if onboarding has not been completed or no state exists.

View File

@@ -24758,6 +24758,129 @@ html .column.drag-over * {
}
}
/* === Post-Onboarding Recommendations === */
.post-onboarding-recommendations {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-md);
padding: var(--space-lg);
background: var(--surface);
border-bottom: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-md);
}
.post-onboarding-recommendations__main {
display: flex;
align-items: flex-start;
gap: var(--space-sm);
min-width: 0;
flex: 1;
}
.post-onboarding-recommendations__icon {
display: flex;
align-items: center;
justify-content: center;
width: calc(var(--space-lg) * 2 + var(--space-xs));
height: calc(var(--space-lg) * 2 + var(--space-xs));
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--primary) 15%, var(--surface));
color: var(--primary);
flex-shrink: 0;
}
.post-onboarding-recommendations__content {
display: flex;
flex-direction: column;
gap: var(--space-xs);
min-width: 0;
}
.post-onboarding-recommendations__title {
margin: 0;
font-size: calc(var(--space-md) + (var(--space-xs) / 2));
font-weight: 600;
color: var(--text);
}
.post-onboarding-recommendations__description {
margin: 0;
font-size: calc(var(--space-md) + (var(--space-xs) / 4));
color: var(--text-muted);
line-height: 1.4;
}
.post-onboarding-recommendations__list {
margin: var(--space-xs) 0 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.post-onboarding-recommendations__item {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--color-warning) 8%, transparent);
}
.post-onboarding-recommendations__item-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: calc(var(--space-sm) * 2);
height: calc(var(--space-sm) * 2);
color: var(--color-warning);
flex-shrink: 0;
}
.post-onboarding-recommendations__item-text {
display: flex;
flex-direction: column;
gap: calc(var(--space-xs) / 2);
font-size: calc(var(--space-md) + (var(--space-xs) / 4));
color: var(--text-muted);
flex: 1;
min-width: 0;
}
.post-onboarding-recommendations__item-text strong {
color: var(--text);
font-weight: 600;
}
.post-onboarding-recommendations__dismiss {
display: inline-flex;
align-items: center;
justify-content: center;
width: calc(var(--space-lg) * 2 + var(--space-xs));
height: calc(var(--space-lg) * 2 + var(--space-xs));
border: var(--btn-border-width) solid transparent;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast);
flex-shrink: 0;
}
.post-onboarding-recommendations__dismiss:hover {
color: var(--text);
background: color-mix(in srgb, var(--text-muted) 12%, transparent);
}
.post-onboarding-recommendations__dismiss:focus-visible,
.post-onboarding-recommendations .btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
/* Complete step */
.model-onboarding-complete {
display: flex;
@@ -34835,4 +34958,31 @@ html .column.drag-over * {
.insight-item-actions {
align-self: flex-end;
}
.post-onboarding-recommendations {
flex-direction: column;
align-items: stretch;
gap: var(--space-sm);
padding: var(--space-md);
}
.post-onboarding-recommendations__main {
width: 100%;
}
.post-onboarding-recommendations__item {
align-items: flex-start;
flex-wrap: wrap;
}
.post-onboarding-recommendations .btn.btn-sm {
margin-left: auto;
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
}
.post-onboarding-recommendations__dismiss {
align-self: flex-end;
min-width: calc(var(--space-lg) * 2 + var(--space-xs));
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
}
}