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",