Skip to content

Commit a15cc68

Browse files
authored
feat(environment): group fork tools and lore (#140)
1 parent 9964390 commit a15cc68

3 files changed

Lines changed: 182 additions & 29 deletions

File tree

apps/web/src/components/chat/environment/EnvironmentPanel.tsx

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { shouldShowStudioFolderRow } from "./EnvironmentPanel.logic";
7272
import {
7373
ENVIRONMENT_ROW_ICON_CLASS_NAME,
7474
EnvironmentCollapsibleSection,
75+
EnvironmentDisclosureGroup,
7576
EnvironmentLabeledSection,
7677
EnvironmentPanelTitle,
7778
EnvironmentRow,
@@ -424,8 +425,6 @@ export function EnvironmentPanel({
424425
dangling rule. Visibility is gated on the per-section AppSettings flags.
425426
*/}
426427
{settings.showEnvironmentUsage ? <EnvironmentUsageSection provider={activeProvider} /> : null}
427-
<EnvironmentAchievementsSection enabled={open} />
428-
429428
{settings.showEnvironmentRepository && githubRepository && onOpenGithubRepository ? (
430429
<EnvironmentLabeledSection label="Repository">
431430
<EnvironmentRow
@@ -454,30 +453,6 @@ export function EnvironmentPanel({
454453
</EnvironmentLabeledSection>
455454
) : null}
456455

457-
{isGitRepo ? (
458-
<div key={gitCwd} className="contents">
459-
<EnvironmentForkHealthSection gitCwd={gitCwd} enabled={open} />
460-
<EnvironmentUpstreamRadarSection gitCwd={gitCwd} enabled={open} />
461-
<EnvironmentAttributionGuardianSection gitCwd={gitCwd} enabled={open} />
462-
<EnvironmentForkArchaeologySection gitCwd={gitCwd} enabled={open} />
463-
<EnvironmentForkFamilyTreeSection gitCwd={gitCwd} enabled={open} />
464-
<EnvironmentOriginalityMeterSection gitCwd={gitCwd} enabled={open} />
465-
<EnvironmentReadmeTruthinessSection gitCwd={gitCwd} enabled={open} />
466-
<EnvironmentForkSpeedrunSection
467-
key={activeProjectId}
468-
gitCwd={gitCwd}
469-
projectId={activeProjectId}
470-
projectCreatedAt={activeProjectCreatedAt}
471-
enabled={open}
472-
/>
473-
<EnvironmentApologyProgressionSection
474-
key={`apology-${activeProjectId}`}
475-
projectId={activeProjectId}
476-
enabled={open}
477-
/>
478-
</div>
479-
) : null}
480-
481456
{settings.showEnvironmentPullRequest && isGitRepo && onOpenGithubRepository ? (
482457
<EnvironmentPullRequestSection
483458
gitCwd={gitCwd}
@@ -572,6 +547,46 @@ export function EnvironmentPanel({
572547
/>
573548
</>
574549
) : null}
550+
551+
{isGitRepo ? (
552+
<div key={gitCwd} className="contents">
553+
<EnvironmentDisclosureGroup label="Fork Tools">
554+
{(groupOpen) => (
555+
<>
556+
<EnvironmentForkHealthSection gitCwd={gitCwd} enabled={open && groupOpen} />
557+
<EnvironmentUpstreamRadarSection gitCwd={gitCwd} enabled={open && groupOpen} />
558+
<EnvironmentAttributionGuardianSection
559+
gitCwd={gitCwd}
560+
enabled={open && groupOpen}
561+
/>
562+
<EnvironmentForkArchaeologySection gitCwd={gitCwd} enabled={open && groupOpen} />
563+
<EnvironmentForkFamilyTreeSection gitCwd={gitCwd} enabled={open && groupOpen} />
564+
</>
565+
)}
566+
</EnvironmentDisclosureGroup>
567+
<EnvironmentDisclosureGroup label="Fork Lore™">
568+
{(groupOpen) => (
569+
<>
570+
<EnvironmentOriginalityMeterSection gitCwd={gitCwd} enabled={open && groupOpen} />
571+
<EnvironmentReadmeTruthinessSection gitCwd={gitCwd} enabled={open && groupOpen} />
572+
<EnvironmentForkSpeedrunSection
573+
key={activeProjectId}
574+
gitCwd={gitCwd}
575+
projectId={activeProjectId}
576+
projectCreatedAt={activeProjectCreatedAt}
577+
enabled={open && groupOpen}
578+
/>
579+
<EnvironmentApologyProgressionSection
580+
key={`apology-${activeProjectId}`}
581+
projectId={activeProjectId}
582+
enabled={open && groupOpen}
583+
/>
584+
<EnvironmentAchievementsSection enabled={open && groupOpen} />
585+
</>
586+
)}
587+
</EnvironmentDisclosureGroup>
588+
</div>
589+
) : null}
575590
</div>
576591
);
577592

apps/web/src/components/chat/environment/EnvironmentRow.browser.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
import "../../../index.css";
66

7-
import { page } from "vitest/browser";
7+
import { page, userEvent } from "vitest/browser";
88
import { afterEach, describe, expect, it, vi } from "vitest";
99
import { render } from "vitest-browser-react";
1010

11-
import { EnvironmentCollapsibleSection } from "./EnvironmentRow";
11+
import {
12+
EnvironmentCollapsibleSection,
13+
EnvironmentDisclosureGroup,
14+
EnvironmentLabeledSection,
15+
} from "./EnvironmentRow";
1216

1317
describe("EnvironmentCollapsibleSection", () => {
1418
afterEach(() => {
@@ -37,3 +41,61 @@ describe("EnvironmentCollapsibleSection", () => {
3741
expect(chevron?.getAttribute("class")).not.toContain("rotate-90");
3842
});
3943
});
44+
45+
describe("EnvironmentDisclosureGroup", () => {
46+
afterEach(() => {
47+
document.body.innerHTML = "";
48+
});
49+
50+
it.each([1_440, 1_280, 1_024])(
51+
"opens with keyboard access and enables its contents at %ipx",
52+
async (width) => {
53+
await page.viewport(width, 800);
54+
await render(
55+
<div className="w-72" data-testid="environment-card">
56+
<EnvironmentDisclosureGroup label="Fork Tools">
57+
{(open) => <span>{open ? "Fork Health ready" : "Expensive read disabled"}</span>}
58+
</EnvironmentDisclosureGroup>
59+
</div>,
60+
);
61+
62+
const trigger = page.getByRole("button", { name: "Fork Tools" });
63+
await expect.element(trigger).toHaveAttribute("aria-expanded", "false");
64+
await expect.element(page.getByText("Fork Health ready")).not.toBeInTheDocument();
65+
66+
document.querySelector<HTMLElement>('[data-slot="collapsible-trigger"]')?.focus();
67+
await userEvent.keyboard("{Enter}");
68+
69+
await expect.element(trigger).toHaveAttribute("aria-expanded", "true");
70+
await expect.element(page.getByText("Fork Health ready")).toBeInTheDocument();
71+
const card = document.querySelector<HTMLElement>("[data-testid='environment-card']");
72+
expect(card?.scrollWidth).toBeLessThanOrEqual(card?.clientWidth ?? 0);
73+
74+
await userEvent.keyboard(" ");
75+
await expect.element(trigger).toHaveAttribute("aria-expanded", "false");
76+
await expect.element(page.getByText("Fork Health ready")).not.toBeInTheDocument();
77+
},
78+
);
79+
80+
it("suppresses a repeated nested group label while preserving distinct section names", async () => {
81+
await render(
82+
<EnvironmentDisclosureGroup label="Fork Lore™">
83+
{() => (
84+
<>
85+
<EnvironmentLabeledSection label="Fork Lore">
86+
<span>Originality Meter™</span>
87+
</EnvironmentLabeledSection>
88+
<EnvironmentLabeledSection label="Achievements">
89+
<span>Local achievements</span>
90+
</EnvironmentLabeledSection>
91+
</>
92+
)}
93+
</EnvironmentDisclosureGroup>,
94+
);
95+
96+
await page.getByRole("button", { name: "Fork Lore™" }).click();
97+
98+
await expect.element(page.getByText("Fork Lore", { exact: true })).not.toBeInTheDocument();
99+
await expect.element(page.getByText("Achievements", { exact: true })).toBeInTheDocument();
100+
});
101+
});

apps/web/src/components/chat/environment/EnvironmentRow.tsx

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
// relocated picker trigger reuses this skin so the rows line up on one grid.
66
// Layer: Environment panel UI primitive
77

8-
import { useState, type ComponentPropsWithoutRef, type ReactNode } from "react";
8+
import {
9+
createContext,
10+
useContext,
11+
useState,
12+
type ComponentPropsWithoutRef,
13+
type ReactNode,
14+
} from "react";
915

1016
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "~/components/ui/collapsible";
1117
import { DisclosureChevron } from "~/components/ui/DisclosureChevron";
@@ -62,6 +68,12 @@ export function EnvironmentSectionLabel({ children }: { children: ReactNode }) {
6268
return <p className={ENVIRONMENT_PANEL_SECTION_LABEL_CLASS_NAME}>{children}</p>;
6369
}
6470

71+
const EnvironmentDisclosureGroupContext = createContext<string | null>(null);
72+
73+
function comparableSectionLabel(label: string): string {
74+
return label.replace(/$/u, "").trim();
75+
}
76+
6577
/** Section label plus one or more rows beneath it — shared by Editor, Usage, Repository, etc. */
6678
export function EnvironmentLabeledSection({
6779
label,
@@ -70,6 +82,25 @@ export function EnvironmentLabeledSection({
7082
label: ReactNode;
7183
children: ReactNode;
7284
}) {
85+
const disclosureGroupLabel = useContext(EnvironmentDisclosureGroupContext);
86+
if (disclosureGroupLabel !== null) {
87+
const repeatsGroupLabel =
88+
typeof label === "string" &&
89+
comparableSectionLabel(label) === comparableSectionLabel(disclosureGroupLabel);
90+
if (repeatsGroupLabel) {
91+
return <div className="flex flex-col gap-0.5">{children}</div>;
92+
}
93+
return (
94+
<section
95+
className="flex flex-col gap-0.5"
96+
aria-label={typeof label === "string" ? label : undefined}
97+
>
98+
<EnvironmentSectionLabel>{label}</EnvironmentSectionLabel>
99+
{children}
100+
</section>
101+
);
102+
}
103+
73104
return (
74105
<>
75106
<EnvironmentSectionDivider />
@@ -81,6 +112,51 @@ export function EnvironmentLabeledSection({
81112
);
82113
}
83114

115+
/**
116+
* Top-level Environment information-architecture group. Groups default closed so the common
117+
* working-state scan stays compact; callers receive the open state so expensive child reads can
118+
* remain disabled until requested.
119+
*/
120+
export function EnvironmentDisclosureGroup({
121+
label,
122+
children,
123+
}: {
124+
label: string;
125+
children: (open: boolean) => ReactNode;
126+
}) {
127+
const [open, setOpen] = useState(false);
128+
return (
129+
<>
130+
<EnvironmentSectionDivider />
131+
<Collapsible open={open} onOpenChange={setOpen} className="flex flex-col">
132+
<CollapsibleTrigger
133+
className={cn(
134+
"group/section flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1 text-left",
135+
"outline-none",
136+
ELEVATED_HOVER_SURFACE_CLASS_NAME,
137+
"focus-visible:bg-[var(--color-background-elevated-secondary)]",
138+
)}
139+
>
140+
<span
141+
className={cn(ENVIRONMENT_PANEL_SECTION_LABEL_INLINE_CLASS_NAME, "min-w-0 truncate")}
142+
>
143+
{label}
144+
</span>
145+
<DisclosureChevron
146+
open={open}
147+
className="size-3 shrink-0 text-[var(--color-text-foreground-secondary)] opacity-60"
148+
/>
149+
</CollapsibleTrigger>
150+
<CollapsiblePanel>
151+
<EnvironmentDisclosureGroupContext.Provider value={label}>
152+
<div className="flex flex-col gap-1 pt-0.5">{children(open)}</div>
153+
</EnvironmentDisclosureGroupContext.Provider>
154+
</CollapsiblePanel>
155+
</Collapsible>
156+
</>
157+
);
158+
}
159+
84160
/**
85161
* Collapsible section: a folder-style header (rotating chevron + section label) that shows or
86162
* hides its children, mirroring the sidebar's project/thread-list disclosure. Built on the shared

0 commit comments

Comments
 (0)