Split app/styles.css from ~40k lines down to ~4.5k. Created 56 co-located
component CSS files in app/components/, each imported by its owning .tsx.
The remainder of styles.css holds genuinely global rules (design tokens,
.btn/.card/.modal/.form-input primitives, cross-component @media overrides).
- Lazy-load 13 heavy views (AgentsView, RoadmapsView, NodesView, etc.) via
React.lazy + Suspense; prefetch all chunks on idle so first navigation is
instant. Initial JS bundle: 1.58 MB → 1.16 MB (-26%). Initial CSS bundle:
635 kB → 471 kB (-26%); the rest splits into 13 per-view chunks.
- Add app/test/cssFixture.ts exposing loadAllAppCss() + loadAllAppCssBaseOnly()
so CSS regression tests load the full per-component bundle (mirroring Vite
source order). Migrate 30+ tests off direct readFileSync('../styles.css').
- Enable test.css: { include: [/.+/] } in vitest.config.ts so component CSS
imports actually inject styles in jsdom (fixes getComputedStyle assertions).
- Add ESLint rule (no-restricted-syntax) banning direct styles.css reads in
dashboard test files; points at loadAllAppCss() instead.
- Restore lost utility classes (.text-muted, .text-secondary, .text-dim,
.form-input) and rescue dropped chat tool-call rules into QuickChatFAB.css.
- Mobile fixes along the way: scroll containment for view containers
(min-height:0 + -webkit-overflow-scrolling), QuickChatFAB full-screen on
mobile (with safe-area-inset for iOS home bar), AgentsView single-row
header layout, ActivityLogModal close button on right, model-combobox
z-index above the mobile quick-chat panel.
- Bug fix: SkillsView toggle was display:none which hid the input from the
accessibility tree; replaced with the visually-hidden pattern so screen
readers + getByRole still find the checkbox.
- Bug fix: standalone Delete button in TaskDetailModal for triage-column
tasks (Actions dropdown is hidden in triage state, so previously no way
to delete a freshly-created task without status change first).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
212 lines
6.6 KiB
TypeScript
212 lines
6.6 KiB
TypeScript
import "./PostOnboardingRecommendations.css";
|
|
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>
|
|
);
|
|
}
|