fix(dashboard): make selection Add-comment trigger clickable and label read-only previews
The global .btn:active press-feedback transform (scale 0.97) overrode the selection-comment trigger's positioning translate, teleporting the fixed-position button mid-press so the click never landed and the composer silently never opened. The :active state now restates the translate alongside the scale on desktop and mobile offsets. Document previews also gain a subtle Read-only badge explaining select-to-comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/artifacts-add-comment-active-transform.md
Normal file
7
.changeset/artifacts-add-comment-active-transform.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Fix the Artifacts preview "Add comment" button doing nothing when clicked, and label the preview as read-only.
|
||||||
|
category: fix
|
||||||
|
dev: The global `.btn:active { transform: scale(0.97) }` press feedback replaced the selection-comment trigger's positioning translate while the mouse was held, moving the button out from under the cursor so `click` never fired. `.selection-comment-trigger:active` now restates the translate (desktop and mobile), covering DocumentsView and FileEditor surfaces. The Artifacts project-file preview header also gains a `documents.readOnly` badge.
|
||||||
@@ -318,6 +318,19 @@ Artifacts controls are the first page content below the shared header, so add a
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ArtifactsView 2026-07-10-16:10:
|
||||||
|
Subtle view-only indicator for the project-file preview header; muted so it informs without competing with the Plain/Markdown toggle. Shared markup serves desktop and mobile.
|
||||||
|
*/
|
||||||
|
.documents-readonly-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.documents-content-viewer-text {
|
.documents-content-viewer-text {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|||||||
@@ -744,6 +744,16 @@ export function DocumentsView({ projectId, addToast, onOpenDetail, onOpenArtifac
|
|||||||
<div className="documents-content-viewer">
|
<div className="documents-content-viewer">
|
||||||
<div className="documents-content-header">
|
<div className="documents-content-header">
|
||||||
<p className="documents-file-path-header">{selectedFile.path}</p>
|
<p className="documents-file-path-header">{selectedFile.path}</p>
|
||||||
|
{/*
|
||||||
|
FNXC:ArtifactsView 2026-07-10-16:10:
|
||||||
|
First-run review feedback: the Artifacts preview pane did not communicate whether documents are editable. This pane is view-only (there is no editor here — editing happens elsewhere, e.g. the workspace FileEditor), so a persistent Read-only badge states that explicitly on both desktop and mobile. Select-to-comment still works and is the intended interaction.
|
||||||
|
*/}
|
||||||
|
<span
|
||||||
|
className="documents-readonly-badge badge"
|
||||||
|
title={t("documents.readOnlyHint", "This preview is read-only. Select text to comment and send it to a new task.")}
|
||||||
|
>
|
||||||
|
{t("documents.readOnly", "Read-only")}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm document-mode-toggle"
|
className="btn btn-sm document-mode-toggle"
|
||||||
onClick={() => setRenderProjectMarkdown((prev) => !prev)}
|
onClick={() => setRenderProjectMarkdown((prev) => !prev)}
|
||||||
|
|||||||
@@ -12,6 +12,24 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ArtifactsView 2026-07-10-16:05:
|
||||||
|
The "Add comment" trigger is positioned entirely by its transform translate. The global
|
||||||
|
`.btn:active { transform: scale(0.97) }` press feedback REPLACED that translate while the
|
||||||
|
mouse button was held, teleporting the trigger about half its width right and one spacing
|
||||||
|
unit down between mousedown and mouseup, so the browser never dispatched `click` on the
|
||||||
|
button and the comment composer silently never opened (user-reported no-op while viewing a
|
||||||
|
markdown file in the Artifacts view; reproduced with real mouse events — jsdom clicks
|
||||||
|
cannot catch this).
|
||||||
|
Every :active override here must restate the positioning translate alongside the press
|
||||||
|
feedback so the trigger stays under the cursor for the whole press. This one shared rule
|
||||||
|
covers all popover surfaces: DocumentsView project-file preview (plain and markdown) and
|
||||||
|
FileEditor (editor and preview), desktop and mobile.
|
||||||
|
*/
|
||||||
|
.selection-comment-trigger:active {
|
||||||
|
transform: translate(-50%, calc(-1 * var(--space-xl))) scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
.selection-comment-panel {
|
.selection-comment-panel {
|
||||||
width: min(var(--selection-comment-panel-width, calc(var(--space-2xl) * 12)), calc(100vw - (var(--space-lg) * 2)));
|
width: min(var(--selection-comment-panel-width, calc(var(--space-2xl) * 12)), calc(100vw - (var(--space-lg) * 2)));
|
||||||
transform: translate(-50%, var(--space-xs));
|
transform: translate(-50%, var(--space-xs));
|
||||||
@@ -62,6 +80,16 @@
|
|||||||
transform: translate(-50%, calc(-1 * var(--space-2xl)));
|
transform: translate(-50%, calc(-1 * var(--space-2xl)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ArtifactsView 2026-07-10-16:05:
|
||||||
|
Mobile uses a taller lift, so its :active rule must restate the mobile translate too —
|
||||||
|
otherwise the desktop :active rule (space-xl) would snap the pressed trigger to the
|
||||||
|
desktop offset and reintroduce the missed-click no-op on touch/landscape-phone widths.
|
||||||
|
*/
|
||||||
|
.selection-comment-trigger:active {
|
||||||
|
transform: translate(-50%, calc(-1 * var(--space-2xl))) scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
.selection-comment-actions {
|
.selection-comment-actions {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,6 +567,24 @@ describe("DocumentsView", () => {
|
|||||||
expect(await screen.findByText(/Hello docs/)).toBeInTheDocument();
|
expect(await screen.findByText(/Hello docs/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ArtifactsView 2026-07-10-16:20:
|
||||||
|
First-run review: the preview pane did not communicate that it is view-only. The Read-only
|
||||||
|
badge must render with the preview header in BOTH render modes (plain and markdown) — the
|
||||||
|
header markup is shared between desktop and mobile layouts.
|
||||||
|
*/
|
||||||
|
it("shows a Read-only badge in the project file preview header in plain and markdown modes", async () => {
|
||||||
|
render(<DocumentsView addToast={addToast} onOpenDetail={onOpenDetail} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Open README.md" }));
|
||||||
|
await screen.findByText(/Hello docs/);
|
||||||
|
expect(screen.getByText("Read-only")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /switch to markdown/i }));
|
||||||
|
await screen.findByText("Hello docs");
|
||||||
|
expect(screen.getByText("Read-only")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("sends selected plain project file preview text to a new task description", async () => {
|
it("sends selected plain project file preview text to a new task description", async () => {
|
||||||
mockSelectionRect();
|
mockSelectionRect();
|
||||||
const onSendSelectionToTask = vi.fn();
|
const onSendSelectionToTask = vi.fn();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { SelectionCommentPopover, composeSelectionCommentDescription } from "../SelectionCommentPopover";
|
import { SelectionCommentPopover, composeSelectionCommentDescription } from "../SelectionCommentPopover";
|
||||||
@@ -65,6 +67,43 @@ describe("SelectionCommentPopover", () => {
|
|||||||
expect(screen.getByRole("button", { name: /add a comment/i })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /add a comment/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:ArtifactsView 2026-07-10-16:20:
|
||||||
|
Regression guard for the "Add comment does nothing" bug: the trigger is positioned solely
|
||||||
|
by its transform translate, and the global `.btn:active { transform: scale(0.97) }` press
|
||||||
|
feedback replaced it while pressed, moving the button out from under the cursor so `click`
|
||||||
|
never fired. jsdom cannot reproduce hit-testing, so this invariant is asserted on the CSS:
|
||||||
|
every `.selection-comment-trigger` transform rule — base AND :active, desktop AND mobile —
|
||||||
|
must include the `translate(-50%` positioning component. The popover is shared by
|
||||||
|
DocumentsView (plain + markdown preview) and FileEditor (editor + preview), so this one
|
||||||
|
stylesheet invariant covers all surfaces.
|
||||||
|
*/
|
||||||
|
it("keeps the positioning translate in every trigger transform, including :active press state", () => {
|
||||||
|
const css = readFileSync(join(__dirname, "..", "SelectionCommentPopover.css"), "utf8");
|
||||||
|
const uncommented = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||||
|
|
||||||
|
// Collect the declaration block of every rule whose selector list targets the trigger.
|
||||||
|
const triggerBlocks: Array<{ selector: string; block: string }> = [];
|
||||||
|
const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
|
||||||
|
for (const match of uncommented.matchAll(rulePattern)) {
|
||||||
|
const selector = match[1].trim();
|
||||||
|
if (selector.split(",").some((part) => part.trim().startsWith(".selection-comment-trigger"))) {
|
||||||
|
triggerBlocks.push({ selector, block: match[2] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformBlocks = triggerBlocks.filter(({ block }) => /transform\s*:/.test(block));
|
||||||
|
// Base + :active on desktop, base + :active in the mobile media query.
|
||||||
|
expect(transformBlocks.length).toBeGreaterThanOrEqual(4);
|
||||||
|
|
||||||
|
for (const { selector, block } of transformBlocks) {
|
||||||
|
expect(block, `trigger transform for "${selector}" must keep the positioning translate`).toContain("translate(-50%");
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeBlocks = transformBlocks.filter(({ selector }) => selector.includes(":active"));
|
||||||
|
expect(activeBlocks.length, "both desktop and mobile need an :active override that restates the translate").toBeGreaterThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses a longer markdown fence when the snippet contains backticks", () => {
|
it("uses a longer markdown fence when the snippet contains backticks", () => {
|
||||||
expect(composeSelectionCommentDescription({
|
expect(composeSelectionCommentDescription({
|
||||||
filePath: "README.md",
|
filePath: "README.md",
|
||||||
|
|||||||
@@ -2250,6 +2250,8 @@
|
|||||||
"projectFiles": "project files",
|
"projectFiles": "project files",
|
||||||
"projectFilesTab": "Project Files",
|
"projectFilesTab": "Project Files",
|
||||||
"projectMarkdownFilesLabel": "Project markdown files",
|
"projectMarkdownFilesLabel": "Project markdown files",
|
||||||
|
"readOnly": "Read-only",
|
||||||
|
"readOnlyHint": "This preview is read-only. Select text to comment and send it to a new task.",
|
||||||
"resultCount_one": "{{count}} result{{plural}}",
|
"resultCount_one": "{{count}} result{{plural}}",
|
||||||
"resultCount_other": "{{count}} result{{plural}}",
|
"resultCount_other": "{{count}} result{{plural}}",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
|
|||||||
@@ -2282,7 +2282,9 @@
|
|||||||
"closeLightbox": "Close artifact preview",
|
"closeLightbox": "Close artifact preview",
|
||||||
"expandArtifact": "Expand {{title}}",
|
"expandArtifact": "Expand {{title}}",
|
||||||
"expandArtifactHint": "Click to expand",
|
"expandArtifactHint": "Click to expand",
|
||||||
"lightboxLabel": "Artifact media preview"
|
"lightboxLabel": "Artifact media preview",
|
||||||
|
"readOnly": "",
|
||||||
|
"readOnlyHint": ""
|
||||||
},
|
},
|
||||||
"droidCli": {
|
"droidCli": {
|
||||||
"active": "Activo",
|
"active": "Activo",
|
||||||
|
|||||||
@@ -2282,7 +2282,9 @@
|
|||||||
"closeLightbox": "Close artifact preview",
|
"closeLightbox": "Close artifact preview",
|
||||||
"expandArtifact": "Expand {{title}}",
|
"expandArtifact": "Expand {{title}}",
|
||||||
"expandArtifactHint": "Click to expand",
|
"expandArtifactHint": "Click to expand",
|
||||||
"lightboxLabel": "Artifact media preview"
|
"lightboxLabel": "Artifact media preview",
|
||||||
|
"readOnly": "",
|
||||||
|
"readOnlyHint": ""
|
||||||
},
|
},
|
||||||
"droidCli": {
|
"droidCli": {
|
||||||
"active": "Actif",
|
"active": "Actif",
|
||||||
|
|||||||
@@ -2282,7 +2282,9 @@
|
|||||||
"closeLightbox": "Close artifact preview",
|
"closeLightbox": "Close artifact preview",
|
||||||
"expandArtifact": "Expand {{title}}",
|
"expandArtifact": "Expand {{title}}",
|
||||||
"expandArtifactHint": "Click to expand",
|
"expandArtifactHint": "Click to expand",
|
||||||
"lightboxLabel": "Artifact media preview"
|
"lightboxLabel": "Artifact media preview",
|
||||||
|
"readOnly": "",
|
||||||
|
"readOnlyHint": ""
|
||||||
},
|
},
|
||||||
"droidCli": {
|
"droidCli": {
|
||||||
"active": "활성",
|
"active": "활성",
|
||||||
|
|||||||
@@ -2282,7 +2282,9 @@
|
|||||||
"closeLightbox": "Close artifact preview",
|
"closeLightbox": "Close artifact preview",
|
||||||
"expandArtifact": "Expand {{title}}",
|
"expandArtifact": "Expand {{title}}",
|
||||||
"expandArtifactHint": "Click to expand",
|
"expandArtifactHint": "Click to expand",
|
||||||
"lightboxLabel": "Artifact media preview"
|
"lightboxLabel": "Artifact media preview",
|
||||||
|
"readOnly": "",
|
||||||
|
"readOnlyHint": ""
|
||||||
},
|
},
|
||||||
"droidCli": {
|
"droidCli": {
|
||||||
"active": "活跃",
|
"active": "活跃",
|
||||||
|
|||||||
@@ -2282,7 +2282,9 @@
|
|||||||
"closeLightbox": "Close artifact preview",
|
"closeLightbox": "Close artifact preview",
|
||||||
"expandArtifact": "Expand {{title}}",
|
"expandArtifact": "Expand {{title}}",
|
||||||
"expandArtifactHint": "Click to expand",
|
"expandArtifactHint": "Click to expand",
|
||||||
"lightboxLabel": "Artifact media preview"
|
"lightboxLabel": "Artifact media preview",
|
||||||
|
"readOnly": "",
|
||||||
|
"readOnlyHint": ""
|
||||||
},
|
},
|
||||||
"droidCli": {
|
"droidCli": {
|
||||||
"active": "活躍",
|
"active": "活躍",
|
||||||
|
|||||||
Reference in New Issue
Block a user