fix(web): keep import controls inside their scroller, so the document stops scrolling (#116)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Opus 5 authored and GitHub committed 2026-07-29 17:45:17 +08:00
1 parent b40262d43c
commit f63037866e
6 files changed
+139 -27

No files matched your search

@@ -27,6 +27,7 @@ import { useProject } from "../../state/project";
import { Tabs } from "../../components/ui/tabs";
import { Button } from "../../components/ui/button";
import { toastError, toastInfo, toastSuccess } from "../../components/ui/toast";
import { HiddenFileInput } from "../../components/ui/hidden-file-input";
import { Input, Textarea } from "../../components/ui/input";
import { OptionMenu, type OptionMenuChoice } from "../../components/ui/option-menu";
import { Switch } from "../../components/ui/switch";
@@ -156,7 +157,9 @@ export function AgentSettingsPage() {
}
return (
<div className="no-scrollbar h-full overflow-y-auto p-4 md:p-6">
/* relative: a scroller is its own containing block (see the invariant in styles.css) —
this page's snapshot-import control used to grow the document from below the fold. */
<div className="no-scrollbar relative h-full overflow-y-auto p-4 md:p-6">
<div className="mx-auto max-w-3xl">
<Button
variant="ghost"
@@ -351,14 +354,7 @@ function OverviewTab({
<label
className={`${TRANSFER_BUTTON_CLASS} ${importing ? "pointer-events-none opacity-60" : ""}`}
>
{/* sr-only rather than hidden: keeps it keyboard-Tab-focusable (same as the workspace-browser upload). */}
<input
type="file"
accept=".tar.gz,.tgz"
className="sr-only"
disabled={importing}
onChange={onPickFile}
/>
<HiddenFileInput accept=".tar.gz,.tgz" disabled={importing} onChange={onPickFile} />
{importing ? S.agent.importing : S.agent.importSnapshot}
</label>
)}
@@ -27,6 +27,7 @@ import { Button } from "../../components/ui/button";
import { ConfirmModal } from "../../components/ui/confirm-modal";
import { toastError, toastSuccess } from "../../components/ui/toast";
import { Dropdown } from "../../components/ui/dropdown";
import { HiddenFileInput } from "../../components/ui/hidden-file-input";
import { SkeletonList } from "../../components/ui/skeleton";
import { CodeBlock } from "./code-block";
@@ -745,14 +746,7 @@ export function WorkspaceBrowser({
</Button>
{/* Matches the same visual style and font size (sm = text-xs) as the adjacent ghost Buttons (Details/Refresh): no border, light background on hover. */}
<label className="inline-flex cursor-pointer items-center rounded-md border border-transparent bg-transparent px-2.5 py-1 text-xs font-medium text-gray-600 transition-colors duration-150 focus-within:ring-2 focus-within:ring-gray-400/30 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100">
{/* sr-only rather than hidden: keyboard users can still Tab-focus it (display:none would remove it from the focus order). */}
<input
type="file"
multiple
className="sr-only"
onChange={onUpload}
disabled={uploading}
/>
<HiddenFileInput multiple onChange={onUpload} disabled={uploading} />
{uploading ? S.common.saving : S.files.upload}
</label>
</div>
@@ -17,6 +17,7 @@ import { formatBytes } from "../../lib/format";
import { agentDisplayName, useProject } from "../../state/project";
import { AgentAvatar } from "../../components/ui/agent-avatar";
import { Chevron } from "../../components/ui/chevron";
import { HiddenFileInput } from "../../components/ui/hidden-file-input";
import { DownloadIcon, UploadIcon } from "../../components/ui/icons";
import { toastError } from "../../components/ui/toast";
import { Truncated } from "../../components/ui/truncated";
@@ -197,14 +198,7 @@ function AgentNode({
importing ? "pointer-events-none opacity-60" : ""
}`}
>
{/* sr-only rather than hidden: keeps it keyboard-Tab-focusable (same as the snapshot import). */}
<input
type="file"
accept=".jsonl"
className="sr-only"
disabled={importing}
onChange={onPickFile}
/>
<HiddenFileInput accept=".jsonl" disabled={importing} onChange={onPickFile} />
<UploadIcon size={13} />
<span className="sr-only">{importing ? S.traces.importing : S.traces.importTrace}</span>
</label>
@@ -297,8 +291,12 @@ export function TracesPage() {
return (
<div className="flex h-full flex-col md:flex-row">
{/* Directory tree: Agent → Session title (≥md left column; <md top collapsible area) */}
<aside className="max-h-52 shrink-0 overflow-y-auto border-b border-gray-200 bg-gray-50 px-1 py-2 md:max-h-none md:w-72 md:border-b-0 md:border-r dark:border-gray-800 dark:bg-gray-900">
{/* Directory tree: Agent → Session title (≥md left column; <md top collapsible area).
relative: a scroller is its own containing block — the invariant and the failure it
prevents are documented in styles.css; this is where it first bit (an Agent node past
the fold put its import control's absolutely positioned box at a document-level
offset, growing the document). */}
<aside className="relative max-h-52 shrink-0 overflow-y-auto border-b border-gray-200 bg-gray-50 px-1 py-2 md:max-h-none md:w-72 md:border-b-0 md:border-r dark:border-gray-800 dark:bg-gray-900">
<p className="px-3 pb-1 text-xs font-bold uppercase tracking-wide text-gray-500">
{S.traces.title}
</p>