refactor(layout): promote AI capture above timeline on desktop

Give the composer full-width desktop space before the event timeline so capture stays prominent and the toolbar can keep its intended two-column composition. Add source-level regression tests to lock the new page hierarchy in place.
This commit is contained in:
2026-04-22 11:34:37 -04:00
parent 88265678db
commit 7df8419368
3 changed files with 55 additions and 42 deletions

View File

@@ -436,10 +436,8 @@ export default function HomePage() {
); );
const appSectionSurfaceClasses = getAppSectionSurfaceClasses(isMobile); const appSectionSurfaceClasses = getAppSectionSurfaceClasses(isMobile);
const appNavSurfaceClasses = getAppNavSurfaceClasses(isMobile); const appNavSurfaceClasses = getAppNavSurfaceClasses(isMobile);
const mainContentClasses = cn( const mainContentClasses = "space-y-4";
"grid items-start gap-4", const timelineContentClasses = "space-y-4";
isMobile ? "grid-cols-1" : "grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]",
);
const headerActionsClasses = isMobile const headerActionsClasses = isMobile
? "flex w-full items-center justify-between gap-2" ? "flex w-full items-center justify-between gap-2"
: "flex flex-wrap items-center justify-end gap-2"; : "flex flex-wrap items-center justify-end gap-2";
@@ -552,44 +550,42 @@ export default function HomePage() {
/> />
) : ( ) : (
<section className={mainContentClasses}> <section className={mainContentClasses}>
<div className="space-y-4"> <section className={appSectionSurfaceClasses}>
<section className={appSectionSurfaceClasses}> <div className="mb-4 space-y-1">
<div className="mb-4 space-y-1"> <p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground"> AI capture
AI capture </p>
</p> <h2 className="text-xl font-semibold tracking-[-0.04em]">
<h2 className="text-xl font-semibold tracking-[-0.04em]"> Capture from text or files.
Capture from text or files. </h2>
</h2> <p className="text-sm leading-6 text-muted-foreground">
<p className="text-sm leading-6 text-muted-foreground"> Start with a prompt, screenshots, or both. Review happens in
Start with a prompt, screenshots, or both. Review happens the timeline rather than a separate flow.
in the timeline rather than a separate flow. </p>
</p> </div>
</div>
<AIToolbar <AIToolbar
adminAiEnabled={adminAiEnabled} adminAiEnabled={adminAiEnabled}
aiEnabled={settings.aiEnabled} aiEnabled={settings.aiEnabled}
isAuthenticated={!!session?.user} isAuthenticated={!!session?.user}
isPending={isPending} isPending={isPending}
aiPrompt={aiPrompt} aiPrompt={aiPrompt}
setAiPrompt={setAiPrompt} setAiPrompt={setAiPrompt}
aiLoading={aiLoading} aiLoading={aiLoading}
imagePreviews={imagePreviews} imagePreviews={imagePreviews}
onImagesSelect={handleImagesSelect} onImagesSelect={handleImagesSelect}
onImageRemove={handleImageRemove} onImageRemove={handleImageRemove}
onAiCreate={handleAiCreate} onAiCreate={handleAiCreate}
onAiTemplateSelect={runAiCreate} onAiTemplateSelect={runAiCreate}
onAiSummarize={handleAiSummarize} onAiSummarize={handleAiSummarize}
onSummaryDismiss={() => setSummary(null)} onSummaryDismiss={() => setSummary(null)}
summary={summary} summary={summary}
summaryUpdated={summaryUpdated} summaryUpdated={summaryUpdated}
events={events} events={events}
/> />
</section> </section>
</div>
<div className="space-y-4"> <section className={timelineContentClasses}>
<section className={appSectionSurfaceClasses}> <section className={appSectionSurfaceClasses}>
<div className="mb-4 flex items-center justify-between gap-3"> <div className="mb-4 flex items-center justify-between gap-3">
<div className="space-y-1"> <div className="space-y-1">
@@ -634,7 +630,7 @@ export default function HomePage() {
onDelete={handleDelete} onDelete={handleDelete}
/> />
</section> </section>
</div> </section>
</section> </section>
)} )}
</main> </main>

View File

@@ -166,6 +166,14 @@ describe("AI capture redesign", () => {
expect(source).not.toContain("lg:grid-cols-"); expect(source).not.toContain("lg:grid-cols-");
}); });
test("desktop composer uses a two-column row when the page gives it full-width space", () => {
const source = readToolbarSource();
expect(source).toContain("isMobile");
expect(source).toContain('? "grid gap-3"');
expect(source).toContain(': "grid gap-3 grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"');
});
test("attachments panel is a first-class surfaced region, not an inline footer affordance", () => { test("attachments panel is a first-class surfaced region, not an inline footer affordance", () => {
const source = readToolbarSource(); const source = readToolbarSource();

View File

@@ -6,13 +6,22 @@ describe("home page hierarchy", () => {
const source = readFileSync("src/app/page.tsx", "utf8"); const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain("useIsMobile"); expect(source).toContain("useIsMobile");
expect(source).toContain("grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]"); expect(source).toContain('const mainContentClasses = "space-y-4"');
expect(source).toContain('const timelineContentClasses = "space-y-4"');
expect(source).toContain('"items-center justify-between"'); expect(source).toContain('"items-center justify-between"');
expect(source).toContain("desktopUtilityActionsClasses"); expect(source).toContain("desktopUtilityActionsClasses");
expect(source).toContain("AI capture"); expect(source).toContain("AI capture");
expect(source).toContain("Event timeline"); expect(source).toContain("Event timeline");
}); });
test("desktop layout promotes AI capture to a full-width top row before the timeline grid", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain("<section className={appSectionSurfaceClasses}>");
expect(source).toContain("<section className={timelineContentClasses}>");
expect(source).not.toContain("grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]");
});
test("manual create is routed through a More menu instead of a primary mobile action", () => { test("manual create is routed through a More menu instead of a primary mobile action", () => {
const source = readFileSync("src/app/page.tsx", "utf8"); const source = readFileSync("src/app/page.tsx", "utf8");