Skip to content

Commit be91050

Browse files
committed
feat: add job-oriented primary input below recommended jobs
1 parent 8284c1e commit be91050

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { CompanyUnderstandingHero } from "@/features/dashboard/components/Compan
1111
import { DashboardAgentRoster } from "@/features/dashboard/components/DashboardAgentRoster";
1212
import { PlaybookInputForm } from "@/features/dashboard/components/PlaybookInputForm";
1313
import { RecommendedJobs } from "@/features/dashboard/components/RecommendedJobs";
14+
import { JobOrientedInput } from "@/features/dashboard/components/JobOrientedInput";
1415
import { useMeaningfulWork } from "@/features/dashboard/hooks/useMeaningfulWork";
1516
import { RecentOutputs } from "@/features/dashboard/components/RecentOutputs";
1617
import { BoardSummary } from "@/features/dashboard/components/BoardSummary";
@@ -180,6 +181,11 @@ function DashboardContent({
180181
/>
181182
</div>
182183

184+
{/* 2.5 Job-oriented free input */}
185+
<div className="dashboard-section">
186+
<JobOrientedInput onSubmit={handleFreeTextSubmit} />
187+
</div>
188+
183189
{/* 3. Specialist roster */}
184190
{!specialistRoster.isLoading && specialistRoster.specialists.length > 0 && (
185191
<div className="dashboard-section">
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useRef, useState } from "react";
2+
import { ArrowRightIcon, PenLineIcon } from "lucide-react";
3+
4+
export interface JobOrientedInputProps {
5+
onSubmit: (text: string) => void;
6+
}
7+
8+
export function JobOrientedInput({ onSubmit }: JobOrientedInputProps) {
9+
const [value, setValue] = useState("");
10+
const textareaRef = useRef<HTMLTextAreaElement>(null);
11+
12+
function handleSubmit() {
13+
const trimmed = value.trim();
14+
if (trimmed) {
15+
onSubmit(trimmed);
16+
setValue("");
17+
if (textareaRef.current) {
18+
textareaRef.current.style.height = "auto";
19+
}
20+
}
21+
}
22+
23+
function handleInput(e: React.ChangeEvent<HTMLTextAreaElement>) {
24+
setValue(e.target.value);
25+
const el = e.target;
26+
el.style.height = "auto";
27+
el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
28+
}
29+
30+
return (
31+
<div>
32+
<div className="mb-3 flex items-center gap-2">
33+
<PenLineIcon className="size-3.5 text-primary" />
34+
<span className="section-label text-primary">YOUR NEXT MOVE</span>
35+
</div>
36+
<div className="group/input rounded-xl bg-gradient-to-r from-border/40 via-border/40 to-border/40 p-[1px] transition-all duration-200 hover:from-border/60 hover:via-border/60 hover:to-border/60 focus-within:from-primary/30 focus-within:via-primary/15 focus-within:to-primary/30 focus-within:shadow-md focus-within:shadow-primary/5 dark:from-white/[0.06] dark:via-white/[0.06] dark:to-white/[0.06] dark:hover:from-white/[0.10] dark:hover:via-white/[0.10] dark:hover:to-white/[0.10] dark:focus-within:from-primary/30 dark:focus-within:via-primary/15 dark:focus-within:to-primary/30">
37+
<div className="relative rounded-[11px] bg-card shadow-sm dark:bg-[#18181B]">
38+
<textarea
39+
ref={textareaRef}
40+
value={value}
41+
onChange={handleInput}
42+
onKeyDown={(e) => {
43+
if (e.key === "Enter" && !e.shiftKey) {
44+
e.preventDefault();
45+
handleSubmit();
46+
}
47+
}}
48+
placeholder="What do you want to get done?"
49+
rows={1}
50+
className="w-full resize-none rounded-[11px] border-0 bg-transparent px-5 py-4 pr-14 text-[15px] leading-relaxed text-foreground placeholder:text-muted-foreground/35 focus:outline-none"
51+
/>
52+
<button
53+
type="button"
54+
onClick={handleSubmit}
55+
disabled={!value.trim()}
56+
className="absolute right-3 bottom-3.5 flex size-8 items-center justify-center rounded-lg text-muted-foreground/40 transition-all duration-100 enabled:bg-primary enabled:text-primary-foreground enabled:shadow-sm hover:enabled:bg-primary/90 disabled:opacity-20"
57+
>
58+
<ArrowRightIcon className="size-4" />
59+
</button>
60+
</div>
61+
</div>
62+
</div>
63+
);
64+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readFileSync, existsSync } from "node:fs";
3+
import { resolve } from "node:path";
4+
5+
const dashDir = resolve(
6+
__dirname,
7+
"../../apps/desktop/src/features/dashboard",
8+
);
9+
const readSrc = (path: string) =>
10+
readFileSync(resolve(dashDir, path), "utf-8");
11+
12+
// ═══════════════════════════════════════════════════════
13+
// 1. JobOrientedInput component
14+
// ═══════════════════════════════════════════════════════
15+
16+
describe("JobOrientedInput component", () => {
17+
const componentPath = resolve(dashDir, "components/JobOrientedInput.tsx");
18+
19+
it("component file exists", () => {
20+
expect(existsSync(componentPath)).toBe(true);
21+
});
22+
23+
it("exports JobOrientedInput", () => {
24+
const src = readSrc("components/JobOrientedInput.tsx");
25+
expect(src).toContain("export function JobOrientedInput");
26+
});
27+
28+
it("uses job-oriented placeholder framing", () => {
29+
const src = readSrc("components/JobOrientedInput.tsx");
30+
// Must contain job-oriented language, not CMO branding
31+
expect(src).toMatch(/get.*done|want to produce|marketing job/i);
32+
});
33+
34+
it("does NOT use CMO branding", () => {
35+
const src = readSrc("components/JobOrientedInput.tsx");
36+
expect(src).not.toContain("Ask CMO");
37+
expect(src).not.toContain("BrainIcon");
38+
expect(src).not.toContain('"CMO"');
39+
});
40+
41+
it("has a textarea for multi-line input", () => {
42+
const src = readSrc("components/JobOrientedInput.tsx");
43+
expect(src).toContain("<textarea");
44+
});
45+
46+
it("handles Enter key for submission", () => {
47+
const src = readSrc("components/JobOrientedInput.tsx");
48+
expect(src).toContain("Enter");
49+
expect(src).toContain("handleSubmit");
50+
});
51+
52+
it("has a submit button with ArrowRight icon", () => {
53+
const src = readSrc("components/JobOrientedInput.tsx");
54+
expect(src).toContain("ArrowRightIcon");
55+
expect(src).toContain("<button");
56+
});
57+
58+
it("accepts onSubmit prop", () => {
59+
const src = readSrc("components/JobOrientedInput.tsx");
60+
expect(src).toContain("onSubmit");
61+
});
62+
63+
it("has a job-oriented section label", () => {
64+
const src = readSrc("components/JobOrientedInput.tsx");
65+
// Should have a section label like "YOUR NEXT MOVE" or similar
66+
expect(src).toContain("section-label");
67+
});
68+
});
69+
70+
// ═══════════════════════════════════════════════════════
71+
// 2. DashboardWorkspace integration
72+
// ═══════════════════════════════════════════════════════
73+
74+
describe("DashboardWorkspace integrates JobOrientedInput", () => {
75+
it("imports JobOrientedInput", () => {
76+
const src = readSrc("components/DashboardWorkspace.tsx");
77+
expect(src).toContain("JobOrientedInput");
78+
});
79+
80+
it("renders JobOrientedInput after RecommendedJobs", () => {
81+
const src = readSrc("components/DashboardWorkspace.tsx");
82+
const jobsIdx = src.indexOf("RecommendedJobs");
83+
const inputIdx = src.indexOf("JobOrientedInput", jobsIdx);
84+
const rosterIdx = src.indexOf("DashboardAgentRoster", inputIdx);
85+
// JobOrientedInput appears between RecommendedJobs and DashboardAgentRoster
86+
expect(inputIdx).toBeGreaterThan(jobsIdx);
87+
expect(rosterIdx).toBeGreaterThan(inputIdx);
88+
});
89+
90+
it("passes handleFreeTextSubmit to JobOrientedInput", () => {
91+
const src = readSrc("components/DashboardWorkspace.tsx");
92+
expect(src).toMatch(/JobOrientedInput[\s\S]*?handleFreeTextSubmit/);
93+
});
94+
});

0 commit comments

Comments
 (0)