fix(ui): combine PRD tabs and remove redundant headers
This commit is contained in:
@@ -14,7 +14,7 @@ type Plan = {
|
|||||||
decisions: Array<{ id: string; title: string; choice: string; why?: string }>;
|
decisions: Array<{ id: string; title: string; choice: string; why?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Tab = "objective" | "stories" | "features" | "architecture";
|
type Tab = "objective" | "prd";
|
||||||
|
|
||||||
// Shared Theme Variables
|
// Shared Theme Variables
|
||||||
const INK = {
|
const INK = {
|
||||||
@@ -78,9 +78,7 @@ export default function PlanPageV2() {
|
|||||||
paddingBottom: 0
|
paddingBottom: 0
|
||||||
}}>
|
}}>
|
||||||
<TabButton active={activeTab === "objective"} onClick={() => setActiveTab("objective")} icon={<Target size={14} />} label="Objective" />
|
<TabButton active={activeTab === "objective"} onClick={() => setActiveTab("objective")} icon={<Target size={14} />} label="Objective" />
|
||||||
<TabButton active={activeTab === "stories"} onClick={() => setActiveTab("stories")} icon={<BookOpen size={14} />} label="User Stories" />
|
<TabButton active={activeTab === "prd"} onClick={() => setActiveTab("prd")} icon={<FileText size={14} />} label="PRD" />
|
||||||
<TabButton active={activeTab === "features"} onClick={() => setActiveTab("features")} icon={<Layers size={14} />} label="Features" />
|
|
||||||
<TabButton active={activeTab === "architecture"} onClick={() => setActiveTab("architecture")} icon={<GitBranch size={14} />} label="Architecture" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -93,16 +91,8 @@ export default function PlanPageV2() {
|
|||||||
<ObjectiveView plan={plan} projectId={projectId} onChange={setPlan} />
|
<ObjectiveView plan={plan} projectId={projectId} onChange={setPlan} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: activeTab === "stories" ? "block" : "none" }}>
|
<div style={{ display: activeTab === "prd" ? "flex" : "none", height: "calc(100vh - 200px)" }}>
|
||||||
<UserStoriesView plan={plan} projectId={projectId} onChange={setPlan} />
|
<PRDView plan={plan} projectId={projectId} onChange={setPlan} />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: activeTab === "features" ? "block" : "none" }}>
|
|
||||||
<FeaturesView plan={plan} projectId={projectId} onChange={setPlan} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: activeTab === "architecture" ? "block" : "none" }}>
|
|
||||||
<ArchitectureView plan={plan} projectId={projectId} onChange={setPlan} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -179,237 +169,133 @@ function ObjectiveView({ plan, projectId, onChange }: { plan: Plan, projectId: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// 2. USER STORIES VIEW
|
// 2. PRD VIEW
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
// MOCK DATA: In production, this would be parsed from the AI's spec markdown or a JSON array in the database.
|
function PRDView({ plan, projectId, onChange }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) {
|
||||||
const MOCK_JOURNEYS = [
|
const [activeDoc, setActiveDoc] = useState("spec");
|
||||||
{
|
|
||||||
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" style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
<div style={{ display: "flex", width: "100%", height: "100%", gap: 24, alignItems: "flex-start" }}>
|
||||||
|
{/* LEFT COLUMN: The Document Index */}
|
||||||
|
<div style={{
|
||||||
<div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
|
width: 250,
|
||||||
|
flexShrink: 0,
|
||||||
{/* LEFT COLUMN: The Index */}
|
display: "flex",
|
||||||
<div style={{
|
flexDirection: "column",
|
||||||
width: "30%",
|
gap: 8
|
||||||
minWidth: 250,
|
}}>
|
||||||
display: "flex",
|
<button
|
||||||
flexDirection: "column",
|
onClick={() => setActiveDoc("spec")}
|
||||||
gap: 8
|
style={{
|
||||||
}}>
|
textAlign: "left",
|
||||||
{MOCK_JOURNEYS.map(j => {
|
padding: "12px 16px",
|
||||||
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,
|
borderRadius: 8,
|
||||||
background: "transparent",
|
background: activeDoc === "spec" ? "#fff" : "transparent",
|
||||||
color: INK.muted,
|
border: activeDoc === "spec" ? `1px solid ${INK.border}` : "1px solid transparent",
|
||||||
fontSize: "0.85rem",
|
boxShadow: activeDoc === "spec" ? "0 1px 3px rgba(0,0,0,0.02)" : "none",
|
||||||
fontWeight: 500,
|
color: activeDoc === "spec" ? INK.main : INK.muted,
|
||||||
|
fontWeight: activeDoc === "spec" ? 600 : 500,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
|
transition: "all 0.15s ease",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
gap: 8
|
||||||
gap: 6
|
}}
|
||||||
}}>
|
>
|
||||||
<Plus size={14} /> Add Journey
|
<BookOpen size={16} /> Product Spec
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<button
|
||||||
|
onClick={() => setActiveDoc("plan")}
|
||||||
{/* RIGHT COLUMN: The Details */}
|
style={{
|
||||||
<div style={{
|
textAlign: "left",
|
||||||
flex: 1,
|
padding: "12px 16px",
|
||||||
background: "#fff",
|
borderRadius: 8,
|
||||||
border: `1px solid ${INK.border}`,
|
background: activeDoc === "plan" ? "#fff" : "transparent",
|
||||||
borderRadius: 8,
|
border: activeDoc === "plan" ? `1px solid ${INK.border}` : "1px solid transparent",
|
||||||
padding: 32,
|
boxShadow: activeDoc === "plan" ? "0 1px 3px rgba(0,0,0,0.02)" : "none",
|
||||||
boxShadow: "0 1px 3px rgba(0,0,0,0.02)"
|
color: activeDoc === "plan" ? INK.main : INK.muted,
|
||||||
}}>
|
fontWeight: activeDoc === "plan" ? 600 : 500,
|
||||||
{activeJourney ? (
|
cursor: "pointer",
|
||||||
<>
|
transition: "all 0.15s ease",
|
||||||
<div style={{ marginBottom: 32, paddingBottom: 24, borderBottom: `1px solid ${INK.border}` }}>
|
display: "flex",
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 12 }}>
|
alignItems: "center",
|
||||||
<h3 style={{ fontSize: "1.4rem", fontWeight: 700, color: INK.main, margin: 0 }}>
|
gap: 8
|
||||||
{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 }}>
|
<GitBranch size={16} /> Tech Architecture
|
||||||
{activeJourney.persona}
|
</button>
|
||||||
</span>
|
<button
|
||||||
</div>
|
onClick={() => setActiveDoc("tasks")}
|
||||||
<p style={{ fontSize: "0.95rem", color: INK.muted, margin: 0, lineHeight: 1.5 }}>
|
style={{
|
||||||
<strong style={{ color: INK.main }}>Goal:</strong> {activeJourney.goal}
|
textAlign: "left",
|
||||||
</p>
|
padding: "12px 16px",
|
||||||
</div>
|
borderRadius: 8,
|
||||||
|
background: activeDoc === "tasks" ? "#fff" : "transparent",
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
|
border: activeDoc === "tasks" ? `1px solid ${INK.border}` : "1px solid transparent",
|
||||||
{activeJourney.stories.map((story, i) => (
|
boxShadow: activeDoc === "tasks" ? "0 1px 3px rgba(0,0,0,0.02)" : "none",
|
||||||
<div key={story.id}>
|
color: activeDoc === "tasks" ? INK.main : INK.muted,
|
||||||
<div style={{ display: "flex", alignItems: "baseline", gap: 12, marginBottom: 8 }}>
|
fontWeight: activeDoc === "tasks" ? 600 : 500,
|
||||||
<span style={{ color: INK.faint, fontWeight: 600, fontSize: "0.85rem" }}>US{i + 1}</span>
|
cursor: "pointer",
|
||||||
<h4 style={{ fontSize: "1.05rem", fontWeight: 600, color: INK.main, margin: 0 }}>{story.title}</h4>
|
transition: "all 0.15s ease",
|
||||||
</div>
|
display: "flex",
|
||||||
<p style={{ fontSize: "0.95rem", color: INK.muted, margin: "0 0 16px 0", fontStyle: "italic" }}>
|
alignItems: "center",
|
||||||
"{story.description}"
|
gap: 8
|
||||||
</p>
|
}}
|
||||||
|
>
|
||||||
<div style={{ background: INK.bgHover, padding: 16, borderRadius: 8, border: `1px solid ${INK.border}` }}>
|
<ListTodo size={16} /> Execution Plan
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
// 3. FEATURES VIEW (Replaces Tasks)
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
function FeaturesView({ plan, projectId }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) {
|
|
||||||
const features = plan.tasks.filter(t => t.status !== "done"); // Simplification for prototype
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="panel-container">
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24 }}>
|
|
||||||
<div>
|
|
||||||
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, color: INK.main }}>Features Queue</h2>
|
|
||||||
<p style={{ color: INK.muted, fontSize: "0.85rem", margin: "4px 0 0" }}>High-level capabilities to be delegated to the AI.</p>
|
|
||||||
</div>
|
|
||||||
<button className="btn-primary">
|
|
||||||
<Plus size={14} /> New Feature
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "grid", gap: 12 }}>
|
{/* RIGHT COLUMN: The Document Content */}
|
||||||
{features.length > 0 ? features.map(f => (
|
<div style={{
|
||||||
<div key={f.id} style={{
|
flex: 1,
|
||||||
border: `1px solid ${INK.border}`,
|
background: "#fff",
|
||||||
borderRadius: 8,
|
border: `1px solid ${INK.border}`,
|
||||||
padding: 16,
|
borderRadius: 8,
|
||||||
background: "#fff",
|
padding: 40,
|
||||||
display: "flex",
|
height: "100%",
|
||||||
justifyContent: "space-between",
|
overflowY: "auto",
|
||||||
alignItems: "center"
|
boxShadow: "0 1px 3px rgba(0,0,0,0.02)"
|
||||||
}}>
|
}}>
|
||||||
<div>
|
{activeDoc === "spec" && (
|
||||||
<div style={{ fontWeight: 600, fontSize: "0.95rem", color: INK.main }}>{f.title}</div>
|
<div className="markdown-prose">
|
||||||
{f.description && <div style={{ fontSize: "0.8rem", color: INK.muted, marginTop: 4 }}>{f.description}</div>}
|
<h1>Product Specification</h1>
|
||||||
|
<p style={{ color: INK.muted }}>The Product Specification document outlines the core user journeys, functional requirements, and acceptance criteria.</p>
|
||||||
|
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
||||||
|
<BookOpen size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
||||||
|
<p style={{ fontWeight: 500 }}>Spec is empty.</p>
|
||||||
|
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Use Architect mode to generate the Product Spec.</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-secondary" style={{ background: INK.main, color: "#fff", borderColor: INK.main }}>
|
|
||||||
Delegate
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)) : (
|
)}
|
||||||
<div style={{ padding: 40, textAlign: "center", border: `1px solid ${INK.border}`, borderRadius: 8, color: INK.muted }}>
|
|
||||||
No features defined yet. Use Architect mode to brainstorm.
|
{activeDoc === "plan" && (
|
||||||
|
<div className="markdown-prose">
|
||||||
|
<h1>Technical Architecture</h1>
|
||||||
|
<p style={{ color: INK.muted }}>The Technical Architecture plan defines the tech stack, database models, and API interfaces required to support the product spec.</p>
|
||||||
|
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
||||||
|
<GitBranch size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
||||||
|
<p style={{ fontWeight: 500 }}>Plan is empty.</p>
|
||||||
|
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Use Architect mode to define the technical approach.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeDoc === "tasks" && (
|
||||||
|
<div className="markdown-prose">
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<h1>Execution Plan</h1>
|
||||||
|
<button className="btn-primary">Delegate Build</button>
|
||||||
|
</div>
|
||||||
|
<p style={{ color: INK.muted }}>The atomic, dependency-ordered tasks the AI will execute to build the application.</p>
|
||||||
|
<hr style={{ margin: "24px 0", borderTop: `1px dashed ${INK.border}`, borderBottom: "none" }} />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: 60, color: INK.muted, textAlign: "center" }}>
|
||||||
|
<ListTodo size={32} style={{ marginBottom: 16, opacity: 0.5 }} />
|
||||||
|
<p style={{ fontWeight: 500 }}>No tasks queued.</p>
|
||||||
|
<p style={{ fontSize: "0.85rem", maxWidth: 300, margin: "8px 0 0" }}>Generate the task breakdown from the Product Spec.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -417,43 +303,6 @@ function FeaturesView({ plan, projectId }: { plan: Plan, projectId: string, onCh
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
// 4. ARCHITECTURE VIEW
|
|
||||||
// ──────────────────────────────────────────────────
|
|
||||||
function ArchitectureView({ plan, projectId }: { plan: Plan, projectId: string, onChange: (p: Plan) => void }) {
|
|
||||||
return (
|
|
||||||
<div className="panel-container">
|
|
||||||
<div style={{ marginBottom: 24 }}>
|
|
||||||
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, margin: 0, color: INK.main }}>Technical Architecture</h2>
|
|
||||||
<p style={{ color: INK.muted, fontSize: "0.85rem", margin: "4px 0 0" }}>Decisions made regarding the tech stack, database, and integrations.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
|
|
||||||
{plan.decisions.length > 0 ? plan.decisions.map(d => (
|
|
||||||
<div key={d.id} style={{ border: `1px solid ${INK.border}`, borderRadius: 8, padding: 20, background: "#fff" }}>
|
|
||||||
<div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em", color: INK.faint, marginBottom: 4 }}>
|
|
||||||
{d.title}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "1.1rem", fontWeight: 600, color: INK.main, marginBottom: 8 }}>
|
|
||||||
{d.choice}
|
|
||||||
</div>
|
|
||||||
{d.why && (
|
|
||||||
<div style={{ fontSize: "0.85rem", color: INK.muted, lineHeight: 1.5 }}>
|
|
||||||
{d.why}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div style={{ gridColumn: "span 2", padding: 40, textAlign: "center", border: `1px dashed ${INK.border}`, borderRadius: 8, color: INK.muted }}>
|
|
||||||
No technical decisions logged yet.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Shared UI Components ──────────────────────────────────────────────────────
|
// ── Shared UI Components ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function TabButton({ active, onClick, icon, label }: { active: boolean, onClick: () => void, icon: React.ReactNode, label: string }) {
|
function TabButton({ active, onClick, icon, label }: { active: boolean, onClick: () => void, icon: React.ReactNode, label: string }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user