From 7ed71ccd3749da3969e9f54e5631f7fa4d5b5d72 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 10 Jul 2026 17:25:13 -0700 Subject: [PATCH] fix(dashboard): keep selection-comment composer fixed-positioned and inside the viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The panel carries the shared .card class, and '.card { position: relative }' loads after the popover stylesheet, so equal specificity let bundle order strip the panel's position: fixed — it then flowed inside the documents viewer and rendered clipped at the bottom-right of the viewport, far from the selection. A .selection-comment-panel.card rule restores fixed positioning immune to order; the panel's left is now a width-aware clamp (no half-offscreen composer near viewport edges), top is clamped near the bottom, and the textarea focuses with preventScroll so opening the composer no longer scrolls the selected content out of view. Co-Authored-By: Claude Fable 5 --- .../components/SelectionCommentPopover.css | 29 +++++++++++++++++-- .../components/SelectionCommentPopover.tsx | 9 +++++- .../SelectionCommentPopover.test.tsx | 29 +++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/dashboard/app/components/SelectionCommentPopover.css b/packages/dashboard/app/components/SelectionCommentPopover.css index a71ddcc3fd..1dd90d52ac 100644 --- a/packages/dashboard/app/components/SelectionCommentPopover.css +++ b/packages/dashboard/app/components/SelectionCommentPopover.css @@ -30,8 +30,26 @@ FileEditor (editor and preview), desktop and mobile. transform: translate(-50%, calc(-1 * var(--space-xl))) scale(0.97); } +/* +FNXC:ArtifactsView 2026-07-10-18:20: +The composer panel carries the shared `.card` class, and `.card { position: relative }` loads AFTER +this file in the bundle, so at equal specificity it silently replaced the panel's `position: fixed`. +The panel then flowed inside `.documents-content-viewer` with the left/top vars applied as relative +offsets — rendering clipped at the bottom-right of the viewport nowhere near the selection. +`.selection-comment-panel.card` (0,2,0) makes the fixed positioning immune to bundle order. +The `left` clamp keeps the whole panel inside the viewport on every width: it accounts for half the +panel width (the panel is centered on the selection via translate(-50%)), and the `top` clamp stops +a selection near the bottom of the viewport from pushing the composer off-screen. +*/ +.selection-comment-panel.card { + position: fixed; +} + .selection-comment-panel { - width: min(var(--selection-comment-panel-width, calc(var(--space-2xl) * 12)), calc(100vw - (var(--space-lg) * 2))); + --scp-width: min(var(--selection-comment-panel-width, calc(var(--space-2xl) * 12)), calc(100vw - (var(--space-lg) * 2))); + width: var(--scp-width); + left: clamp(calc(var(--space-lg) + (var(--scp-width) / 2)), var(--selection-comment-left), calc(100vw - var(--space-lg) - (var(--scp-width) / 2))); + top: min(var(--selection-comment-top), calc(100vh - var(--space-lg) - 20rem)); transform: translate(-50%, var(--space-xs)); padding: var(--space-md); display: flex; @@ -71,8 +89,13 @@ FileEditor (editor and preview), desktop and mobile. } @media (max-width: 768px) { - .selection-comment-trigger, - .selection-comment-panel { + /* + FNXC:ArtifactsView 2026-07-10-18:20: + Only the trigger keeps the simple center-point clamp here — the panel's base rule already applies + a width-aware clamp on all viewports, and re-declaring `left` in this later block would override + it with a clamp that lets half the panel hang past the edge. + */ + .selection-comment-trigger { left: max(var(--space-lg), min(var(--selection-comment-left), calc(100vw - var(--space-lg)))); } diff --git a/packages/dashboard/app/components/SelectionCommentPopover.tsx b/packages/dashboard/app/components/SelectionCommentPopover.tsx index efd8d8045e..267ae759bc 100644 --- a/packages/dashboard/app/components/SelectionCommentPopover.tsx +++ b/packages/dashboard/app/components/SelectionCommentPopover.tsx @@ -87,7 +87,14 @@ export function SelectionCommentPopover({ useEffect(() => { if (!expanded) return; - textareaRef.current?.focus(); + /* + FNXC:ArtifactsView 2026-07-10-18:20: + The panel is position:fixed, so the default focus scroll-into-view is meaningless for it — but + the browser still scrolled the underlying preview pane, yanking the selected content out of + view the moment the composer opened. preventScroll keeps the pane exactly where the user + selected the text. + */ + textareaRef.current?.focus({ preventScroll: true }); }, [expanded]); const setPanelExpanded = useCallback((open: boolean) => { diff --git a/packages/dashboard/app/components/__tests__/SelectionCommentPopover.test.tsx b/packages/dashboard/app/components/__tests__/SelectionCommentPopover.test.tsx index 97b08d8240..9945cfbfab 100644 --- a/packages/dashboard/app/components/__tests__/SelectionCommentPopover.test.tsx +++ b/packages/dashboard/app/components/__tests__/SelectionCommentPopover.test.tsx @@ -104,6 +104,35 @@ describe("SelectionCommentPopover", () => { expect(activeBlocks.length, "both desktop and mobile need an :active override that restates the translate").toBeGreaterThanOrEqual(2); }); + /* + FNXC:ArtifactsView 2026-07-10-18:20: + Regression guard for the composer-panel drift: the panel carries the shared `.card` class, and + `.card { position: relative }` loads after this stylesheet, so a bare `.selection-comment-panel` + rule lost `position: fixed` to bundle order and the panel rendered clipped at the viewport's + bottom-right, far from the selection. The fixed positioning must live on a selector that + out-specifies `.card` regardless of order, and the panel's `left` must be a width-aware clamp so + a selection near a viewport edge cannot push half the panel off-screen. + */ + it("keeps the composer panel fixed-positioned over .card and clamps it inside the viewport", () => { + const css = readFileSync(join(__dirname, "..", "SelectionCommentPopover.css"), "utf8"); + const uncommented = css.replace(/\/\*[\s\S]*?\*\//g, ""); + + const rulePattern = /([^{}]+)\{([^{}]*)\}/g; + const blocks = [...uncommented.matchAll(rulePattern)].map((m) => ({ selector: m[1].trim(), block: m[2] })); + + const fixedOverCard = blocks.find(({ selector, block }) => + selector.split(",").some((part) => { + const s = part.trim(); + return s.includes(".selection-comment-panel") && s.includes(".card"); + }) && /position\s*:\s*fixed/.test(block)); + expect(fixedOverCard, "a .selection-comment-panel selector compounded with .card must restate position: fixed").toBeTruthy(); + + const panelBlocks = blocks.filter(({ selector }) => selector.split(",").some((p) => p.trim() === ".selection-comment-panel")); + const clampBlock = panelBlocks.find(({ block }) => /left\s*:\s*clamp\(/.test(block)); + expect(clampBlock, "panel must declare a width-aware left clamp").toBeTruthy(); + expect(clampBlock!.block, "left clamp must account for half the panel width").toContain("--scp-width) / 2"); + }); + it("uses a longer markdown fence when the snippet contains backticks", () => { expect(composeSelectionCommentDescription({ filePath: "README.md",