Skip to content

Commit 6f441fb

Browse files
committed
fix draft launcher narrow carousel
1 parent 2174213 commit 6f441fb

4 files changed

Lines changed: 175 additions & 10 deletions

File tree

packages/web/src/features/agent-panes/views/shared/draft-launcher.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,36 @@ describe("DraftLauncher", () => {
125125
expect(screen.getByText("Select Agent")).toBeInTheDocument();
126126
});
127127

128+
it("switches draft launcher carousel panels", () => {
129+
const store = createStore();
130+
131+
store.set(localeAtom, "en");
132+
store.set(wsClientAtom, {
133+
sendCommand: vi.fn(),
134+
subscribe: vi.fn(() => () => {}),
135+
} as never);
136+
137+
const { container } = render(
138+
<Provider store={store}>
139+
<DraftLauncher workspaceId="ws-123" />
140+
</Provider>
141+
);
142+
143+
const agentButton = screen.getByRole("button", { name: "Agent" });
144+
const fileButton = screen.getByRole("button", { name: "File Editor" });
145+
const carouselTrack = container.querySelector(".agent-draft-component-row");
146+
147+
expect(agentButton).toHaveAttribute("aria-pressed", "true");
148+
expect(fileButton).toHaveAttribute("aria-pressed", "false");
149+
expect(carouselTrack).not.toHaveClass("agent-draft-component-row--file");
150+
151+
fireEvent.click(fileButton);
152+
153+
expect(agentButton).toHaveAttribute("aria-pressed", "false");
154+
expect(fileButton).toHaveAttribute("aria-pressed", "true");
155+
expect(carouselTrack).toHaveClass("agent-draft-component-row--file");
156+
});
157+
128158
it("renders a draft drop label when pane drag hover is active", () => {
129159
const store = createStore();
130160

packages/web/src/features/agent-panes/views/shared/draft-launcher.tsx

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Session } from "@coder-studio/core";
22
import { useAtomValue, useSetAtom } from "jotai";
33
import { ArrowRight, FlipHorizontal, FlipVertical, X } from "lucide-react";
4-
import { type DragEvent, type FC, useState } from "react";
4+
import { type DragEvent, type FC, type PointerEvent, useRef, useState } from "react";
55
import { dispatchCommandAtom } from "../../../../atoms/connection";
66
import { sessionsAtom } from "../../../../atoms/sessions";
77
import { Button, IconButton, StatusDot, Tag, ThemedIcon, Tooltip } from "../../../../components/ui";
@@ -46,7 +46,9 @@ export const DraftLauncher: FC<DraftLauncherProps> = ({
4646
const t = useTranslation();
4747
const dispatch = useAtomValue(dispatchCommandAtom);
4848
const setSessions = useSetAtom(sessionsAtom);
49+
const [activePanel, setActivePanel] = useState<"agent" | "file">("agent");
4950
const [isFileDropTarget, setIsFileDropTarget] = useState(false);
51+
const swipeStartXRef = useRef<number | null>(null);
5052
const { states, launch } = useProviderLauncher(
5153
dispatch,
5254
workspaceId,
@@ -189,6 +191,34 @@ export const DraftLauncher: FC<DraftLauncherProps> = ({
189191
onOpenFile?.(paneId, path);
190192
};
191193

194+
const handleCarouselPointerDown = (event: PointerEvent<HTMLDivElement>) => {
195+
if (event.pointerType === "mouse") {
196+
return;
197+
}
198+
199+
swipeStartXRef.current = event.clientX;
200+
};
201+
202+
const handleCarouselPointerUp = (event: PointerEvent<HTMLDivElement>) => {
203+
const startX = swipeStartXRef.current;
204+
swipeStartXRef.current = null;
205+
206+
if (startX === null || event.pointerType === "mouse") {
207+
return;
208+
}
209+
210+
const deltaX = event.clientX - startX;
211+
if (Math.abs(deltaX) < 48) {
212+
return;
213+
}
214+
215+
setActivePanel(deltaX < 0 ? "file" : "agent");
216+
};
217+
218+
const handleCarouselPointerCancel = () => {
219+
swipeStartXRef.current = null;
220+
};
221+
192222
return (
193223
<div
194224
className={`session-card agent-pane${dragState?.isDragging ? " draft-launcher--dragging" : ""}${dragState?.isActiveDropTarget || isFileDropTarget ? " draft-launcher--drop-target" : ""}`}
@@ -254,7 +284,12 @@ export const DraftLauncher: FC<DraftLauncherProps> = ({
254284
<div className="agent-draft-launcher">
255285
<div className="agent-draft-content">
256286
<div className="agent-draft-component">
257-
<div className="agent-draft-component-row">
287+
<div
288+
className={`agent-draft-component-row${activePanel === "file" ? " agent-draft-component-row--file" : ""}`}
289+
onPointerCancel={handleCarouselPointerCancel}
290+
onPointerDown={handleCarouselPointerDown}
291+
onPointerUp={handleCarouselPointerUp}
292+
>
258293
{/* Agent panel */}
259294
<div className="agent-draft-panel">
260295
<div className="agent-draft-panel-header">
@@ -384,6 +419,22 @@ export const DraftLauncher: FC<DraftLauncherProps> = ({
384419
</div>
385420
</div>
386421

422+
<div className="agent-draft-carousel-dots" role="group" aria-label="Draft panels">
423+
{[
424+
{ id: "agent" as const, label: "Agent" },
425+
{ id: "file" as const, label: "File Editor" },
426+
].map((panel) => (
427+
<button
428+
key={panel.id}
429+
aria-label={panel.label}
430+
aria-pressed={activePanel === panel.id}
431+
className={`agent-draft-carousel-dot${activePanel === panel.id ? " agent-draft-carousel-dot--active" : ""}`}
432+
onClick={() => setActivePanel(panel.id)}
433+
type="button"
434+
/>
435+
))}
436+
</div>
437+
387438
<div className="agent-draft-footer">点击启动 Agent 或直接拖拽文件到右侧区域打开</div>
388439
</div>
389440
</div>

packages/web/src/styles/components.css

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7279,6 +7279,8 @@ textarea.input {
72797279
}
72807280

72817281
.agent-draft-content {
7282+
width: 100%;
7283+
max-width: 100%;
72827284
align-items: center;
72837285
justify-content: center;
72847286
}
@@ -7288,12 +7290,47 @@ textarea.input {
72887290
display: flex;
72897291
flex-direction: column;
72907292
gap: var(--sp-2);
7293+
width: fit-content;
7294+
max-width: 100%;
72917295
}
72927296

72937297
.agent-draft-component-row {
72947298
display: flex;
72957299
flex-direction: row;
7300+
align-items: stretch;
72967301
gap: var(--sp-4);
7302+
transition: transform var(--duration-normal) var(--ease-out);
7303+
}
7304+
7305+
.agent-draft-carousel-dots {
7306+
display: none;
7307+
justify-content: center;
7308+
gap: var(--sp-2);
7309+
padding-top: var(--sp-2);
7310+
}
7311+
7312+
.agent-draft-carousel-dot {
7313+
appearance: none;
7314+
border: 0;
7315+
width: 6px;
7316+
height: 6px;
7317+
border-radius: var(--radius-full);
7318+
background: var(--text-disabled);
7319+
padding: 0;
7320+
cursor: pointer;
7321+
transition:
7322+
width var(--duration-fast) var(--ease-out),
7323+
background-color var(--duration-fast) var(--ease-out);
7324+
}
7325+
7326+
.agent-draft-carousel-dot:focus-visible {
7327+
outline: 2px solid var(--status-info-fg);
7328+
outline-offset: 2px;
7329+
}
7330+
7331+
.agent-draft-carousel-dot--active {
7332+
width: 18px;
7333+
background: var(--text-primary);
72977334
}
72987335

72997336
/* ── Panel ────────────────────────────────────────── */
@@ -7499,15 +7536,47 @@ textarea.input {
74997536
}
75007537

75017538
@container (max-width: 28rem) {
7539+
.agent-draft-component {
7540+
width: min(100%, 24rem);
7541+
overflow: hidden;
7542+
}
7543+
75027544
.agent-draft-component-row {
7503-
flex-direction: column;
7545+
width: 200%;
7546+
flex-direction: row;
7547+
gap: 0;
7548+
}
7549+
7550+
.agent-draft-component-row--file {
7551+
transform: translateX(-50%);
7552+
}
7553+
7554+
.agent-draft-panel {
7555+
flex: 0 0 50%;
7556+
width: 50%;
7557+
align-items: stretch;
7558+
justify-content: center;
75047559
}
75057560

75067561
.agent-draft-panel + .agent-draft-panel {
75077562
border-left: none;
7508-
border-top: 1px solid var(--border-default);
7563+
border-top: none;
75097564
padding-left: var(--sp-5);
75107565
}
7566+
7567+
.agent-draft-panel-header {
7568+
align-self: flex-start;
7569+
}
7570+
7571+
.agent-draft-panel-list,
7572+
.agent-draft-drop-zone,
7573+
.agent-provider-card {
7574+
width: 100%;
7575+
}
7576+
7577+
.agent-draft-carousel-dots {
7578+
display: flex;
7579+
}
75117580
}
75127581

75137582
.session-card {

packages/web/src/styles/components.theme.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,9 +1814,13 @@ describe("components.css theme-sensitive surfaces", () => {
18141814
expect(stylesheet).not.toContain("\n.agent-provider-card[disabled] {\n");
18151815
});
18161816

1817-
it("keeps draft launcher provider cards adaptive inside narrow panes", () => {
1817+
it("keeps draft launcher provider cards and carousel adaptive inside narrow panes", () => {
18181818
const launcher = getLastRuleBlock(".agent-draft-launcher");
18191819
const content = getLastRuleBlock(".agent-draft-content");
1820+
const component = getLastRuleBlock(".agent-draft-component");
1821+
const carouselRow = getLastRuleBlock(".agent-draft-component-row");
1822+
const carouselDots = getLastRuleBlock(".agent-draft-carousel-dots");
1823+
const activeCarouselDot = getLastRuleBlock(".agent-draft-carousel-dot--active");
18201824
const providerCard = getLastRuleBlock(".agent-provider-card");
18211825
const providerBody = getLastRuleBlock(".agent-provider-card-body");
18221826
const providerArrow = getLastRuleBlock(".agent-provider-card-arrow");
@@ -1825,9 +1829,22 @@ describe("components.css theme-sensitive surfaces", () => {
18251829
const providerIcon = getLastRuleBlock(".agent-provider-card-icon");
18261830

18271831
expect(launcher).toContain("container-type: inline-size");
1832+
expect(content).toContain("width: 100%");
18281833
expect(content).toContain("max-width: 100%");
1829-
expect(content).toContain("background: var(--agent-draft-surface)");
1830-
expect(content).toContain("backdrop-filter: var(--material-backdrop-filter)");
1834+
expect(component).toContain("max-width: 100%");
1835+
expect(carouselRow).toContain("width: 200%");
1836+
expect(carouselRow).toContain("flex-direction: row");
1837+
expect(carouselRow).toContain("gap: 0");
1838+
expect(getLastRuleBlock(".agent-draft-component-row--file")).toContain(
1839+
"transform: translateX(-50%)"
1840+
);
1841+
expect(
1842+
getRuleBlocksFrom(stylesheet, ".agent-draft-carousel-dots").some((block) =>
1843+
block.includes("display: none")
1844+
)
1845+
).toBe(true);
1846+
expect(carouselDots).toContain("display: flex");
1847+
expect(activeCarouselDot).toContain("width: 18px");
18311848
expect(providerCard).toContain("background: var(--agent-provider-card-surface)");
18321849
expect(providerCard).toContain("min-width: 0");
18331850
expect(providerBody).toContain("width: 100%");
@@ -1838,9 +1855,7 @@ describe("components.css theme-sensitive surfaces", () => {
18381855
expect(codexCard).toContain("background: var(--state-info-bg)");
18391856
expect(codexCard).toContain("border-color: var(--state-info-border)");
18401857
expect(providerIcon).toContain("background: var(--icon-surface-subtle)");
1841-
expect(stylesheet).toMatch(
1842-
/@container\s*\(max-width:\s*36rem\)\s*\{[\s\S]*?\.agent-draft-providers\s*\{[\s\S]*?grid-template-columns:\s*1fr;[\s\S]*?\}/
1843-
);
1858+
expect(carouselRow).not.toContain("flex-direction: column");
18441859
});
18451860

18461861
it("keeps legacy agent pane chrome aligned to shared density and state tokens", () => {

0 commit comments

Comments
 (0)