feat(ui): redesign Plan tab with Objective, User Stories, Features, and Architecture panels
This commit is contained in:
@@ -181,22 +181,193 @@ function ObjectiveView({ plan, projectId, onChange }: { plan: Plan, projectId: s
|
|||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// 2. USER STORIES VIEW
|
// 2. USER STORIES VIEW
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
function UserStoriesView({ plan, projectId }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) {
|
// MOCK DATA: In production, this would be parsed from the AI's spec markdown or a JSON array in the database.
|
||||||
// In a full implementation, we'd parse spec.md or filter specific types of tasks.
|
const MOCK_JOURNEYS = [
|
||||||
// For now, we mock the UI to show the intended architecture.
|
{
|
||||||
|
id: "j1",
|
||||||
|
name: "User Dashboard",
|
||||||
|
persona: "👑 Admin",
|
||||||
|
goal: "Provides a centralized view of all active jobs, metrics, and pending approvals.",
|
||||||
|
stories: [
|
||||||
|
{
|
||||||
|
id: "s1",
|
||||||
|
title: "View active jobs",
|
||||||
|
description: "As an Admin, I want to see a map of today's jobs so I know where my team is.",
|
||||||
|
criteria: [
|
||||||
|
{ text: "Map centers on user's GPS location.", done: false },
|
||||||
|
{ text: "Clicking a pin opens a detail modal.", done: true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "s2",
|
||||||
|
title: "Approve pending payouts",
|
||||||
|
description: "As an Admin, I want to review completed jobs and click 'Approve' to trigger Stripe payouts.",
|
||||||
|
criteria: [
|
||||||
|
{ text: "List displays jobs with 'completed' status.", done: false },
|
||||||
|
{ text: "Approve button triggers success toast.", done: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "j2",
|
||||||
|
name: "Authentication & Onboarding",
|
||||||
|
persona: "👤 Customer",
|
||||||
|
goal: "Allows a new user to securely create an account and fill out their initial profile.",
|
||||||
|
stories: [
|
||||||
|
{
|
||||||
|
id: "s3",
|
||||||
|
title: "Sign up via Email",
|
||||||
|
description: "As a Customer, I want to sign up with my email and a password so my data is secure.",
|
||||||
|
criteria: [
|
||||||
|
{ text: "Form validates email format.", done: false },
|
||||||
|
{ text: "Password must be > 8 characters.", done: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function UserStoriesView({ plan, projectId, onChange }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) {
|
||||||
|
const [activeJourneyId, setActiveJourneyId] = useState<string>(MOCK_JOURNEYS[0].id);
|
||||||
|
const activeJourney = MOCK_JOURNEYS.find(j => j.id === activeJourneyId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-container">
|
<div className="panel-container" style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, color: INK.main }}>User Stories</h2>
|
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, color: INK.main }}>User Stories</h2>
|
||||||
<p style={{ color: INK.muted, fontSize: "0.85rem", margin: "4px 0 0" }}>The prioritized journeys users will take through your application.</p>
|
<p style={{ color: INK.muted, fontSize: "0.85rem", margin: "4px 0 0" }}>The prioritized journeys users will take through your application.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ border: `1px dashed ${INK.border}`, padding: 40, borderRadius: 8, textAlign: "center", color: INK.muted }}>
|
<div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
|
||||||
<BookOpen size={24} style={{ margin: "0 auto 12px", opacity: 0.5 }} />
|
|
||||||
<p style={{ fontWeight: 500 }}>User Stories are being built.</p>
|
{/* LEFT COLUMN: The Index */}
|
||||||
<p style={{ fontSize: "0.85rem", maxWidth: 400, margin: "8px auto 0" }}>
|
<div style={{
|
||||||
This view will display specific user personas and their acceptance scenarios (e.g. "As a User, I want to log in so I can see my dashboard").
|
width: "30%",
|
||||||
</p>
|
minWidth: 250,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 8
|
||||||
|
}}>
|
||||||
|
{MOCK_JOURNEYS.map(j => {
|
||||||
|
const isActive = j.id === activeJourneyId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={j.id}
|
||||||
|
onClick={() => setActiveJourneyId(j.id)}
|
||||||
|
style={{
|
||||||
|
textAlign: "left",
|
||||||
|
padding: "16px 16px",
|
||||||
|
borderRadius: 8,
|
||||||
|
background: isActive ? "#fff" : "transparent",
|
||||||
|
border: isActive ? `1px solid ${INK.border}` : "1px solid transparent",
|
||||||
|
boxShadow: isActive ? "0 1px 3px rgba(0,0,0,0.02)" : "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.15s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 6 }}>
|
||||||
|
<span style={{ fontWeight: 600, fontSize: "0.95rem", color: isActive ? INK.main : INK.muted }}>
|
||||||
|
{j.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
|
||||||
|
<span style={{ fontSize: "0.8rem", color: INK.muted, background: INK.bgHover, padding: "2px 6px", borderRadius: 4, border: `1px solid ${INK.border}` }}>
|
||||||
|
{j.persona}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: "0.8rem", color: INK.faint }}>
|
||||||
|
{j.stories.length} stories
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<button style={{
|
||||||
|
marginTop: 8,
|
||||||
|
padding: "12px",
|
||||||
|
border: `1px dashed ${INK.border}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
background: "transparent",
|
||||||
|
color: INK.muted,
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 6
|
||||||
|
}}>
|
||||||
|
<Plus size={14} /> Add Journey
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT COLUMN: The Details */}
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
background: "#fff",
|
||||||
|
border: `1px solid ${INK.border}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 32,
|
||||||
|
boxShadow: "0 1px 3px rgba(0,0,0,0.02)"
|
||||||
|
}}>
|
||||||
|
{activeJourney ? (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: 32, paddingBottom: 24, borderBottom: `1px solid ${INK.border}` }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 12 }}>
|
||||||
|
<h3 style={{ fontSize: "1.4rem", fontWeight: 700, color: INK.main, margin: 0 }}>
|
||||||
|
{activeJourney.name}
|
||||||
|
</h3>
|
||||||
|
<span style={{ fontSize: "0.85rem", color: INK.muted, background: INK.bgHover, padding: "4px 8px", borderRadius: 6, border: `1px solid ${INK.border}`, fontWeight: 500 }}>
|
||||||
|
{activeJourney.persona}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "0.95rem", color: INK.muted, margin: 0, lineHeight: 1.5 }}>
|
||||||
|
<strong style={{ color: INK.main }}>Goal:</strong> {activeJourney.goal}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
|
||||||
|
{activeJourney.stories.map((story, i) => (
|
||||||
|
<div key={story.id}>
|
||||||
|
<div style={{ display: "flex", alignItems: "baseline", gap: 12, marginBottom: 8 }}>
|
||||||
|
<span style={{ color: INK.faint, fontWeight: 600, fontSize: "0.85rem" }}>US{i + 1}</span>
|
||||||
|
<h4 style={{ fontSize: "1.05rem", fontWeight: 600, color: INK.main, margin: 0 }}>{story.title}</h4>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: "0.95rem", color: INK.muted, margin: "0 0 16px 0", fontStyle: "italic" }}>
|
||||||
|
"{story.description}"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ background: INK.bgHover, padding: 16, borderRadius: 8, border: `1px solid ${INK.border}` }}>
|
||||||
|
<div style={{ fontSize: "0.8rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", color: INK.muted, marginBottom: 12 }}>
|
||||||
|
Acceptance Criteria
|
||||||
|
</div>
|
||||||
|
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
|
||||||
|
{story.criteria.map((c, idx) => (
|
||||||
|
<li key={idx} style={{ display: "flex", alignItems: "flex-start", gap: 10, fontSize: "0.9rem", color: INK.main }}>
|
||||||
|
<div style={{
|
||||||
|
width: 16, height: 16, borderRadius: 4, border: `1px solid ${c.done ? "var(--accent)" : INK.faint}`,
|
||||||
|
background: c.done ? "var(--accent)" : "transparent",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, marginTop: 2
|
||||||
|
}}>
|
||||||
|
{c.done && <Check size={12} color="#fff" strokeWidth={3} />}
|
||||||
|
</div>
|
||||||
|
<span style={{ textDecoration: c.done ? "line-through" : "none", opacity: c.done ? 0.5 : 1 }}>
|
||||||
|
{c.text}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: "center", color: INK.muted, padding: 40 }}>Select a journey to view stories</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user