Skip to content

Commit 2940967

Browse files
committed
style: add card backgrounds to Brain page sections for visual separation
Split Brain markdown content (Product/Market/Growth) by h2 headings and wrap each section in a subtle card container with rounded corners, border, and theme-aware background. Adds consistent 12px spacing between sections.
1 parent 75db7fa commit 2940967

File tree

2 files changed

+134
-3
lines changed

2 files changed

+134
-3
lines changed

apps/desktop/src/features/brain/components/BrainWorkspace.tsx

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -646,9 +646,7 @@ function BrainEditor({
646646
) : section.id === "company-context" ? (
647647
<MemoryContentView content={content} onEdit={() => setIsEditing(true)} />
648648
) : (
649-
<div className={KNOWLEDGE_PROSE_CLASSES}>
650-
<Markdown remarkPlugins={[remarkGfm]}>{stripLeadingH1(content)}</Markdown>
651-
</div>
649+
<SectionedMarkdownView content={content} />
652650
)}
653651
</div>
654652
</div>
@@ -849,6 +847,72 @@ function KnowledgeEmptyState({
849847
const KNOWLEDGE_PROSE_CLASSES =
850848
"prose prose-sm dark:prose-invert max-w-prose [&>h1:first-child]:hidden [&_h1]:text-lg [&_h1]:font-display [&_h1]:font-bold [&_h1]:mb-4 [&_h1]:mt-0 [&_h2]:font-mono [&_h2]:text-[11px] [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-[0.08em] [&_h2]:text-primary [&_h2]:mb-4 [&_h2]:mt-10 [&_h2]:pt-6 [&_h2]:border-t [&_h2]:border-border/20 [&_h3]:text-[14px] [&_h3]:font-display [&_h3]:font-bold [&_h3]:tracking-tight [&_h3]:mb-2 [&_h3]:mt-6 [&_h4]:text-[13px] [&_h4]:font-semibold [&_h4]:mb-2 [&_h4]:mt-6 [&_p]:text-[13px] [&_p]:leading-relaxed [&_p]:text-foreground/80 [&_p]:mb-3 [&_ul]:text-[13px] [&_ul]:text-foreground/80 [&_ul]:mb-3 [&_ul]:pl-4 [&_ol]:text-[13px] [&_ol]:text-foreground/80 [&_ol]:mb-3 [&_ol]:pl-4 [&_li]:mb-1.5 [&_li]:leading-relaxed [&_strong]:text-foreground [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 [&_code]:text-[11px] [&_code]:font-mono [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_pre]:bg-muted [&_pre]:rounded-lg [&_pre]:p-3 [&_pre]:text-[11px] [&_pre]:font-mono [&_pre]:overflow-x-auto [&_hr]:border-border/30 [&_hr]:my-6 [&_table]:w-full [&_th]:text-left [&_th]:font-mono [&_th]:text-[10px] [&_th]:font-semibold [&_th]:uppercase [&_th]:tracking-wider [&_th]:text-muted-foreground [&_th]:pb-2 [&_th]:border-b [&_td]:text-[13px] [&_td]:py-1.5 [&_td]:border-b [&_td]:border-border/20 [&_blockquote]:border-l-2 [&_blockquote]:border-primary/30 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground";
851849

850+
/** Prose classes for card-wrapped sections — no h2 top border/margin since each card is self-contained */
851+
const SECTION_CARD_PROSE_CLASSES =
852+
"prose prose-sm dark:prose-invert max-w-none [&>h1:first-child]:hidden [&_h1]:text-lg [&_h1]:font-display [&_h1]:font-bold [&_h1]:mb-4 [&_h1]:mt-0 [&_h2]:font-mono [&_h2]:text-[11px] [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-[0.08em] [&_h2]:text-primary [&_h2]:mb-3 [&_h2]:mt-0 [&_h3]:text-[14px] [&_h3]:font-display [&_h3]:font-bold [&_h3]:tracking-tight [&_h3]:mb-2 [&_h3]:mt-4 [&_h4]:text-[13px] [&_h4]:font-semibold [&_h4]:mb-2 [&_h4]:mt-4 [&_p]:text-[13px] [&_p]:leading-relaxed [&_p]:text-foreground/80 [&_p]:mb-3 [&_ul]:text-[13px] [&_ul]:text-foreground/80 [&_ul]:mb-3 [&_ul]:pl-4 [&_ol]:text-[13px] [&_ol]:text-foreground/80 [&_ol]:mb-3 [&_ol]:pl-4 [&_li]:mb-1.5 [&_li]:leading-relaxed [&_strong]:text-foreground [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 [&_code]:text-[11px] [&_code]:font-mono [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_pre]:bg-muted [&_pre]:rounded-lg [&_pre]:p-3 [&_pre]:text-[11px] [&_pre]:font-mono [&_pre]:overflow-x-auto [&_hr]:border-border/30 [&_hr]:my-4 [&_table]:w-full [&_th]:text-left [&_th]:font-mono [&_th]:text-[10px] [&_th]:font-semibold [&_th]:uppercase [&_th]:tracking-wider [&_th]:text-muted-foreground [&_th]:pb-2 [&_th]:border-b [&_td]:text-[13px] [&_td]:py-1.5 [&_td]:border-b [&_td]:border-border/20 [&_blockquote]:border-l-2 [&_blockquote]:border-primary/30 [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground";
853+
854+
// ---------------------------------------------------------------------------
855+
// Sectioned markdown view — splits content by h2 and wraps each in a card
856+
// ---------------------------------------------------------------------------
857+
858+
function SectionedMarkdownView({ content }: { content: string }) {
859+
const stripped = stripLeadingH1(content);
860+
861+
// Split content into h2 sections
862+
const sections: Array<{ heading: string | null; body: string }> = [];
863+
const lines = stripped.split("\n");
864+
let currentHeading: string | null = null;
865+
let currentBody: string[] = [];
866+
867+
for (const line of lines) {
868+
const h2Match = line.match(/^##\s+(.+)/);
869+
if (h2Match) {
870+
sections.push({ heading: currentHeading, body: currentBody.join("\n") });
871+
currentHeading = h2Match[1]!;
872+
currentBody = [];
873+
} else {
874+
currentBody.push(line);
875+
}
876+
}
877+
sections.push({ heading: currentHeading, body: currentBody.join("\n") });
878+
879+
// Filter out empty preamble sections
880+
const nonEmpty = sections.filter(
881+
(sec) => sec.heading || sec.body.trim(),
882+
);
883+
884+
// If there are no h2 sections, render as a single card
885+
if (nonEmpty.length <= 1 && !nonEmpty[0]?.heading) {
886+
return (
887+
<div className="rounded-lg border border-border/50 bg-card p-5">
888+
<div className={SECTION_CARD_PROSE_CLASSES}>
889+
<Markdown remarkPlugins={[remarkGfm]}>{stripped}</Markdown>
890+
</div>
891+
</div>
892+
);
893+
}
894+
895+
return (
896+
<div className="space-y-3">
897+
{nonEmpty.map((sec, i) => {
898+
const md = sec.heading
899+
? `## ${sec.heading}\n${sec.body}`
900+
: sec.body;
901+
return (
902+
<div
903+
key={sec.heading ?? `section-${i}`}
904+
className="rounded-lg border border-border/50 bg-card p-5"
905+
>
906+
<div className={SECTION_CARD_PROSE_CLASSES}>
907+
<Markdown remarkPlugins={[remarkGfm]}>{md}</Markdown>
908+
</div>
909+
</div>
910+
);
911+
})}
912+
</div>
913+
);
914+
}
915+
852916
function KnowledgeInlineEmpty({
853917
icon: Icon,
854918
title,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readFileSync } from "node:fs";
3+
import { resolve } from "node:path";
4+
5+
const src = readFileSync(
6+
resolve(__dirname, "../../apps/desktop/src/features/brain/components/BrainWorkspace.tsx"),
7+
"utf-8",
8+
);
9+
10+
describe("Brain section card backgrounds — visual separation", () => {
11+
// AC1: A dedicated component wraps markdown h2 sections in card containers
12+
it("has a SectionedMarkdownView component", () => {
13+
expect(src).toContain("function SectionedMarkdownView");
14+
});
15+
16+
it("BrainEditor uses SectionedMarkdownView for markdown sections", () => {
17+
expect(src).toContain("<SectionedMarkdownView");
18+
});
19+
20+
// AC2: Each section card has subtle background, border, and rounded corners
21+
it("section cards use a theme-aware background", () => {
22+
const fnStart = src.indexOf("function SectionedMarkdownView");
23+
expect(fnStart).toBeGreaterThan(0);
24+
const fnEnd = src.indexOf("\nfunction ", fnStart + 1);
25+
const fnBody = src.slice(fnStart, fnEnd > 0 ? fnEnd : undefined);
26+
expect(fnBody).toMatch(/bg-card|bg-muted\/\d/);
27+
});
28+
29+
it("section cards have rounded corners", () => {
30+
const fnStart = src.indexOf("function SectionedMarkdownView");
31+
const fnEnd = src.indexOf("\nfunction ", fnStart + 1);
32+
const fnBody = src.slice(fnStart, fnEnd > 0 ? fnEnd : undefined);
33+
expect(fnBody).toContain("rounded-lg");
34+
});
35+
36+
it("section cards have a border", () => {
37+
const fnStart = src.indexOf("function SectionedMarkdownView");
38+
const fnEnd = src.indexOf("\nfunction ", fnStart + 1);
39+
const fnBody = src.slice(fnStart, fnEnd > 0 ? fnEnd : undefined);
40+
expect(fnBody).toContain("border");
41+
});
42+
43+
it("section cards have internal padding", () => {
44+
const fnStart = src.indexOf("function SectionedMarkdownView");
45+
const fnEnd = src.indexOf("\nfunction ", fnStart + 1);
46+
const fnBody = src.slice(fnStart, fnEnd > 0 ? fnEnd : undefined);
47+
expect(fnBody).toMatch(/p-[45]|px-[45]/);
48+
});
49+
50+
// AC3: Consistent spacing between section cards (12-16px gap)
51+
it("section cards container has vertical spacing", () => {
52+
const fnStart = src.indexOf("function SectionedMarkdownView");
53+
const fnEnd = src.indexOf("\nfunction ", fnStart + 1);
54+
const fnBody = src.slice(fnStart, fnEnd > 0 ? fnEnd : undefined);
55+
// space-y-3 = 12px, space-y-4 = 16px, gap-3 = 12px, gap-4 = 16px
56+
expect(fnBody).toMatch(/space-y-[34]|gap-[34]/);
57+
});
58+
59+
// AC4: Markdown is split by h2 headings
60+
it("splits content by h2 headings", () => {
61+
const fnStart = src.indexOf("function SectionedMarkdownView");
62+
const fnEnd = src.indexOf("\nfunction ", fnStart + 1);
63+
const fnBody = src.slice(fnStart, fnEnd > 0 ? fnEnd : undefined);
64+
// Should split by h2 pattern
65+
expect(fnBody).toMatch(/##\s/);
66+
});
67+
});

0 commit comments

Comments
 (0)