Skip to content

Commit 4c3d61c

Browse files
committed
feat: compress company understanding hero into a focused strip
Redesign CompanyUnderstandingHero from a large hero card with gradient background, 2-column grid, and embedded FreeTextInput into a compact strip showing company identity, product summary, ICP/buyer hint, and 2-3 inline opportunity/risk bullets. Remove sub-component dependencies (HeroOpportunityBullets, HeroRecommendedMove, FreeTextInput) and simplify the props interface.
1 parent 6f9b945 commit 4c3d61c

File tree

4 files changed

+125
-106
lines changed

4 files changed

+125
-106
lines changed
Lines changed: 61 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
import { useState } from "react";
2-
import { GlobeIcon } from "lucide-react";
2+
import { AlertTriangleIcon, GlobeIcon, TrendingUpIcon, UsersIcon } from "lucide-react";
33
import { Skeleton } from "@/components/ui/skeleton";
44
import type { CompanySummaryData } from "@/features/dashboard/lib/parse-workspace-summary";
5-
import type { Opportunity } from "@/features/dashboard/data/opportunities";
6-
import type { HeroRecommendation } from "@/features/dashboard/lib/hero-recommendation";
7-
import { HeroOpportunityBullets } from "@/features/dashboard/components/HeroOpportunityBullets";
8-
import { HeroRecommendedMove } from "@/features/dashboard/components/HeroRecommendedMove";
9-
import { FreeTextInput } from "@/features/dashboard/components/FreeTextInput";
105

116
export interface CompanyUnderstandingHeroProps {
127
domain?: string | undefined;
138
faviconSources?: string[] | undefined;
149
data: CompanySummaryData | null;
15-
opportunities: Opportunity[];
16-
recommendation: HeroRecommendation | null;
1710
isLoading: boolean;
1811
error?: string | null;
19-
onFreeTextSubmit: (text: string) => void;
20-
onActionClick?: (actionId: string) => void;
2112
}
2213

2314
function FaviconIcon({
@@ -48,20 +39,12 @@ function FaviconIcon({
4839

4940
function HeroSkeleton() {
5041
return (
51-
<div className="space-y-5">
52-
<div className="flex items-center gap-4">
53-
<Skeleton className="size-11 rounded-xl" />
54-
<div className="flex-1 space-y-2">
55-
<Skeleton className="h-6 w-40" />
56-
<Skeleton className="h-4 w-full max-w-md" />
57-
</div>
58-
</div>
59-
<div className="space-y-2">
60-
<Skeleton className="h-3 w-28" />
61-
<Skeleton className="h-4 w-3/4" />
62-
<Skeleton className="h-4 w-2/3" />
42+
<div className="flex items-center gap-3">
43+
<Skeleton className="size-8 shrink-0 rounded-lg" />
44+
<div className="flex-1 space-y-1.5">
45+
<Skeleton className="h-5 w-36" />
46+
<Skeleton className="h-3.5 w-full max-w-sm" />
6347
</div>
64-
<Skeleton className="h-12 w-full rounded-xl" />
6548
</div>
6649
);
6750
}
@@ -70,17 +53,12 @@ export function CompanyUnderstandingHero({
7053
domain,
7154
faviconSources,
7255
data,
73-
opportunities,
74-
recommendation,
7556
isLoading,
7657
error,
77-
onFreeTextSubmit,
78-
onActionClick,
7958
}: CompanyUnderstandingHeroProps) {
8059
if (isLoading) {
8160
return (
82-
<div className="relative mb-8 overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-card to-background/60 p-7 shadow-sm dark:border-white/[0.06] dark:from-white/[0.03] dark:to-transparent dark:shadow-none">
83-
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-transparent via-primary/50 to-transparent" />
61+
<div className="mb-6 rounded-xl border border-border/40 px-5 py-4 dark:border-white/[0.06]">
8462
<HeroSkeleton />
8563
</div>
8664
);
@@ -89,65 +67,79 @@ export function CompanyUnderstandingHero({
8967
const hasAnyData = data ? Object.values(data).some(Boolean) : false;
9068
const hasFavicon = domain && faviconSources && faviconSources.length > 0;
9169

92-
return (
93-
<div className="relative mb-8 overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-br from-card via-card to-primary/[0.02] p-7 shadow-sm dark:border-white/[0.06] dark:from-white/[0.03] dark:via-white/[0.02] dark:to-primary/[0.03] dark:shadow-none">
94-
{/* Top accent line */}
95-
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-transparent via-primary/50 to-transparent" />
70+
// Build inline bullets from available data
71+
const bullets: { key: string; icon: "opportunity" | "risk"; text: string }[] = [];
72+
if (data?.topOpportunity) {
73+
bullets.push({ key: "opportunity", icon: "opportunity", text: data.topOpportunity });
74+
}
75+
if (data?.mainRisk) {
76+
bullets.push({ key: "risk", icon: "risk", text: data.mainRisk });
77+
}
9678

97-
{/* ── Company identity ── */}
98-
<div className="flex items-center gap-4">
79+
return (
80+
<div className="mb-6 rounded-xl border border-border/40 px-5 py-4 dark:border-white/[0.06]">
81+
{/* ── Company identity row ── */}
82+
<div className="flex items-center gap-3">
9983
{hasFavicon ? (
100-
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-card shadow-sm ring-1 ring-border/30 dark:bg-white/[0.06] dark:ring-white/[0.08]">
84+
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-card ring-1 ring-border/30 dark:bg-white/[0.06] dark:ring-white/[0.08]">
10185
<FaviconIcon
10286
domain={domain}
10387
faviconSources={faviconSources}
104-
className="size-7 rounded-sm"
88+
className="size-5 rounded-sm"
10589
/>
10690
</div>
10791
) : (
108-
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/8 text-primary">
109-
<GlobeIcon className="size-5" />
92+
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-primary">
93+
<GlobeIcon className="size-4" />
11094
</div>
11195
)}
11296
<div className="min-w-0 flex-1">
113-
<h2 className="font-display text-[24px] font-bold tracking-[-0.02em] text-foreground">
97+
<h2 className="font-display text-[18px] font-bold tracking-[-0.01em] text-foreground">
11498
{domain ?? "Project"}
11599
</h2>
116-
{hasAnyData && data?.productSummary ? (
117-
<p className="mt-1 line-clamp-3 text-[15px] leading-relaxed text-zinc-500 dark:text-zinc-400">
118-
{data.productSummary}
119-
</p>
120-
) : error ? (
121-
<p className="mt-1 text-[13px] text-muted-foreground">
122-
Unable to load project context
123-
</p>
124-
) : (
125-
<p className="mt-1 text-[13px] text-muted-foreground">
126-
No project context yet
127-
</p>
128-
)}
129100
</div>
130101
</div>
131102

132-
{/* ── Opportunity bullets + Recommended move ── */}
133-
{hasAnyData && (
134-
<div className="mt-6 grid gap-5 sm:grid-cols-2">
135-
<HeroOpportunityBullets
136-
opportunities={opportunities}
137-
mainRisk={data?.mainRisk ?? null}
138-
topOpportunity={data?.topOpportunity ?? null}
139-
/>
140-
<HeroRecommendedMove
141-
recommendation={recommendation}
142-
onActionClick={onActionClick}
143-
/>
103+
{/* ── Summary + ICP hint ── */}
104+
{hasAnyData && data?.productSummary ? (
105+
<div className="mt-2 pl-11">
106+
<p className="line-clamp-2 text-[14px] leading-snug text-zinc-500 dark:text-zinc-400">
107+
{data.productSummary}
108+
</p>
109+
{data.targetAudience && (
110+
<p className="mt-1 flex items-center gap-1.5 text-[13px] text-zinc-400 dark:text-zinc-500">
111+
<UsersIcon className="size-3 shrink-0" />
112+
<span className="line-clamp-1">{data.targetAudience}</span>
113+
</p>
114+
)}
144115
</div>
116+
) : error ? (
117+
<p className="mt-2 pl-11 text-[13px] text-muted-foreground">
118+
Unable to load project context
119+
</p>
120+
) : (
121+
<p className="mt-2 pl-11 text-[13px] text-muted-foreground">
122+
No project context yet
123+
</p>
145124
)}
146125

147-
{/* ── CMO input embedded in hero ── */}
148-
<div className="mt-6">
149-
<FreeTextInput onSubmit={onFreeTextSubmit} />
150-
</div>
126+
{/* ── Inline opportunity/risk bullets ── */}
127+
{bullets.length > 0 && (
128+
<ul className="mt-2.5 space-y-1 pl-11">
129+
{bullets.map((b) => (
130+
<li key={b.key} className="flex items-start gap-2">
131+
{b.icon === "risk" ? (
132+
<AlertTriangleIcon className="mt-[3px] size-3 shrink-0 text-amber-500" />
133+
) : (
134+
<TrendingUpIcon className="mt-[3px] size-3 shrink-0 text-primary" />
135+
)}
136+
<span className="line-clamp-1 text-[13px] leading-snug text-zinc-500 dark:text-zinc-400">
137+
{b.text}
138+
</span>
139+
</li>
140+
))}
141+
</ul>
142+
)}
151143
</div>
152144
);
153145
}

apps/desktop/src/features/dashboard/components/DashboardWorkspace.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,22 +162,13 @@ function DashboardContent({
162162
return (
163163
<div className="flex flex-1 flex-col overflow-y-auto p-5 lg:p-6">
164164
<div className="mx-auto w-full max-w-[1000px]">
165-
{/* 1. Company Understanding Hero */}
165+
{/* 1. Company Understanding Strip */}
166166
<CompanyUnderstandingHero
167167
domain={domain}
168168
faviconSources={faviconSources}
169169
data={data}
170-
opportunities={opportunities}
171-
recommendation={heroRecommendation}
172170
isLoading={isLoading}
173171
error={error}
174-
onFreeTextSubmit={handleFreeTextSubmit}
175-
onActionClick={(actionId) => {
176-
const action = starterActions.find((a) => a.id === actionId);
177-
if (action) {
178-
void playbook.handleActionOrPlaybookClick(actionId, action.prompt, action.title);
179-
}
180-
}}
181172
/>
182173

183174
{/* 2. Recommended starting jobs */}

test/ui/dashboard-company-understanding-hero.test.ts

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ describe("pickBestFirstMove", () => {
6363
});
6464

6565
// ═══════════════════════════════════════════════════════
66-
// 2. CompanyUnderstandingHero — main component
66+
// 2. CompanyUnderstandingHero — compact strip
6767
// ═══════════════════════════════════════════════════════
6868

69-
describe("CompanyUnderstandingHero component", () => {
69+
describe("CompanyUnderstandingHero compact strip", () => {
7070
const heroPath = resolve(dashDir, "components/CompanyUnderstandingHero.tsx");
7171

7272
it("exists", () => {
@@ -83,26 +83,50 @@ describe("CompanyUnderstandingHero component", () => {
8383
expect(src).toContain("<FaviconIcon");
8484
});
8585

86-
it("renders full productSummary paragraph (not truncated to one sentence)", () => {
86+
it("renders productSummary", () => {
8787
const src = readSrc("components/CompanyUnderstandingHero.tsx");
88-
// Should show full productSummary, not split on first sentence
8988
expect(src).toContain("data.productSummary");
90-
expect(src).not.toContain("split(/\\.\\s/)");
9189
});
9290

93-
it("renders HeroOpportunityBullets sub-component", () => {
91+
it("renders targetAudience as ICP/buyer hint", () => {
9492
const src = readSrc("components/CompanyUnderstandingHero.tsx");
95-
expect(src).toContain("<HeroOpportunityBullets");
93+
expect(src).toContain("data.targetAudience");
9694
});
9795

98-
it("renders HeroRecommendedMove sub-component", () => {
96+
it("renders topOpportunity as an inline bullet", () => {
9997
const src = readSrc("components/CompanyUnderstandingHero.tsx");
100-
expect(src).toContain("<HeroRecommendedMove");
98+
expect(src).toContain("data.topOpportunity");
10199
});
102100

103-
it("embeds FreeTextInput inside the hero", () => {
101+
it("renders mainRisk as an inline bullet", () => {
104102
const src = readSrc("components/CompanyUnderstandingHero.tsx");
105-
expect(src).toContain("<FreeTextInput");
103+
expect(src).toContain("data.mainRisk");
104+
});
105+
106+
it("does NOT import HeroOpportunityBullets", () => {
107+
const src = readSrc("components/CompanyUnderstandingHero.tsx");
108+
expect(src).not.toContain("HeroOpportunityBullets");
109+
});
110+
111+
it("does NOT import HeroRecommendedMove", () => {
112+
const src = readSrc("components/CompanyUnderstandingHero.tsx");
113+
expect(src).not.toContain("HeroRecommendedMove");
114+
});
115+
116+
it("does NOT import FreeTextInput", () => {
117+
const src = readSrc("components/CompanyUnderstandingHero.tsx");
118+
expect(src).not.toContain("FreeTextInput");
119+
});
120+
121+
it("has compact strip styling (not large hero)", () => {
122+
const src = readSrc("components/CompanyUnderstandingHero.tsx");
123+
// Compact padding and border radius
124+
expect(src).toContain("px-5");
125+
expect(src).toContain("py-4");
126+
expect(src).toContain("rounded-xl");
127+
// Should NOT have large hero padding
128+
expect(src).not.toContain("p-7");
129+
expect(src).not.toContain("rounded-2xl");
106130
});
107131

108132
it("has loading skeleton state", () => {
@@ -124,24 +148,37 @@ describe("CompanyUnderstandingHero component", () => {
124148
it("uses DESIGN.md typography — General Sans for heading", () => {
125149
const src = readSrc("components/CompanyUnderstandingHero.tsx");
126150
expect(src).toContain("font-display");
127-
expect(src).toContain('text-[24px]');
128151
expect(src).toContain("font-bold");
129152
});
130153

131-
it("uses DESIGN.md body text — 15px for summary", () => {
154+
it("uses DESIGN.md secondary text size for buyer hint", () => {
155+
const src = readSrc("components/CompanyUnderstandingHero.tsx");
156+
expect(src).toContain("text-[13px]");
157+
});
158+
159+
it("does not have gradient background", () => {
160+
const src = readSrc("components/CompanyUnderstandingHero.tsx");
161+
expect(src).not.toContain("bg-gradient-to-br");
162+
expect(src).not.toContain("bg-gradient-to-b");
163+
});
164+
165+
it("does not have opportunities or recommendation props", () => {
132166
const src = readSrc("components/CompanyUnderstandingHero.tsx");
133-
expect(src).toContain("text-[15px]");
167+
expect(src).not.toContain("opportunities:");
168+
expect(src).not.toContain("recommendation:");
169+
expect(src).not.toContain("onFreeTextSubmit:");
170+
expect(src).not.toContain("onActionClick?:");
134171
});
135172

136-
it("stays under 200 lines", () => {
173+
it("stays under 150 lines", () => {
137174
const src = readSrc("components/CompanyUnderstandingHero.tsx");
138175
const lineCount = src.split("\n").length;
139-
expect(lineCount).toBeLessThanOrEqual(200);
176+
expect(lineCount).toBeLessThanOrEqual(150);
140177
});
141178
});
142179

143180
// ═══════════════════════════════════════════════════════
144-
// 3. HeroOpportunityBullets — sub-component
181+
// 3. HeroOpportunityBullets — sub-component (still exists)
145182
// ═══════════════════════════════════════════════════════
146183

147184
describe("HeroOpportunityBullets", () => {
@@ -210,7 +247,7 @@ describe("HeroOpportunityBullets", () => {
210247
});
211248

212249
// ═══════════════════════════════════════════════════════
213-
// 4. HeroRecommendedMove — sub-component
250+
// 4. HeroRecommendedMove — sub-component (still exists)
214251
// ═══════════════════════════════════════════════════════
215252

216253
describe("HeroRecommendedMove", () => {
@@ -284,14 +321,13 @@ describe("DashboardWorkspace hero wiring", () => {
284321
expect(src).toContain("pickBestFirstMove(opportunities");
285322
});
286323

287-
it("passes opportunities and recommendation to hero", () => {
288-
expect(src).toContain("opportunities={opportunities}");
289-
expect(src).toContain("recommendation={heroRecommendation}");
324+
it("does not pass opportunities or recommendation to hero", () => {
325+
// These props are removed from the compact strip
326+
expect(src).not.toContain("opportunities={opportunities}");
327+
expect(src).not.toContain("recommendation={heroRecommendation}");
290328
});
291329

292-
it("does not render FreeTextInput directly (now inside hero)", () => {
293-
// FreeTextInput should only appear inside CompanyUnderstandingHero
294-
const directFreeTextRender = src.match(/<FreeTextInput/g);
295-
expect(directFreeTextRender).toBeNull();
330+
it("does not pass onFreeTextSubmit to hero", () => {
331+
expect(src).not.toContain("onFreeTextSubmit={");
296332
});
297333
});

test/ui/dashboard-simplify.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ describe("Dashboard simplification — Sprint 5 reset", () => {
5757
expect(workspaceSrc).toContain("<CompanyUnderstandingHero");
5858
});
5959

60-
it("FreeTextInput is embedded inside CompanyUnderstandingHero", () => {
60+
it("FreeTextInput is not embedded inside CompanyUnderstandingHero (moved to standalone)", () => {
6161
const heroSrc = readSrc("components/CompanyUnderstandingHero.tsx");
62-
expect(heroSrc).toContain("<FreeTextInput");
62+
expect(heroSrc).not.toContain("<FreeTextInput");
6363
});
6464

6565
it("renders RecommendedJobs (replaces SuggestedActionGrid)", () => {

0 commit comments

Comments
 (0)