fix(dashboard): keep selection-comment composer fixed-positioned and inside the viewport
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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))));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user