{
+ let markup = renderToStaticMarkup(
+ {}}
+ onSend={() => {}}
+ showClose={false}
+ />,
+ );
+
+ expect(markup).toContain('aria-label="Post comment"');
+ expect(markup).toContain('data-inset-send="true"');
+ expect(markup).not.toContain(">Comment");
+ expect(markup).not.toContain(">Cancel");
+ });
+
+ it("uses one inset reply action and orders resolution outcomes", () => {
+ let markup = render(view("open"), true);
+
+ expect(markup).toContain('aria-label="Send reply"');
+ expect(markup).toContain('data-plan-comment-composer-shell="true"');
+ expect(markup).toContain("plan-comment-composer-field field");
+ expect(markup).not.toContain(">Reply");
+ expect(markup).toContain("Apply feedback");
+ expect(markup.indexOf("Dismiss")).toBeLessThan(markup.indexOf("Apply feedback"));
+ });
+});
diff --git a/packages/editor/src/comments.tsx b/packages/editor/src/comments.tsx
index 97b16bc7..20c96d76 100644
--- a/packages/editor/src/comments.tsx
+++ b/packages/editor/src/comments.tsx
@@ -7,112 +7,95 @@
* decision. A dismissed one is not rendered at all.
*
* Accept and dismiss both confirm on a second click, because neither can be
- * undone: accepting freezes the thread, puts a decision in the plan and starts
+ * undone: accepting freezes the thread, puts a decision in the document and starts
* a turn. That is the same two-click shape `QuestionView` uses for cancelling,
* so it is an interaction people have already met here.
*/
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useLayoutEffect, useRef, useState } from "react";
+import { ArrowUpIcon, ChatCircleIcon, CheckIcon, XIcon } from "@phosphor-icons/react";
import { limits } from "@chopin/dialect";
import { Provenance, SidecarCard, when } from "./card";
-import { Count } from "./count";
-import { cursor } from "./cursor";
-import type { KeyboardEvent } from "react";
+import type { KeyboardEvent, ReactNode } from "react";
import type { Comment } from "@chopin/protocol";
import type { ThreadView } from "./threads";
function Who({ handle }: { handle: string }) {
return (
-
+
@{handle}
);
}
-function Note({ note }: { note: Comment.Note }) {
+function Note({
+ action,
+ note,
+ opening,
+}: {
+ action?: ReactNode;
+ note: Comment.Note;
+ opening?: boolean;
+}) {
return (
-
-
-
- {when(note.ts)}
+
+
+
+
+
+ {when(note.ts)}
+
+
+ {action}
{note.text}
);
}
-const QUOTED = "m-0 w-full text-left text-sm text-text-secondary italic";
+function CloseButton({ onClose }: { onClose: () => void }) {
+ return (
+
+ );
+}
-/**
- * The prose the thread marks, as a quotation the card can be read without.
- *
- * A button when there is somewhere to go, a blockquote when there is not —
- * which is the rule `QuestionView` already applies to an answer, so both halves
- * of the sidecar offer the same thing in the same way. A real element carries
- * the affordance and the keyboard handling rather than a quotation pretending
- * to be one, and a drifted thread offers no jump it could not honour: the card
- * still reads, which is the durable part of a comment.
- *
- * The quote rather than the card. A card holds a reply box, an Accept and a
- * Dismiss; making the whole of it a link would mean deciding, on every click,
- * whether the reader meant the link or the control they actually hit.
- *
- * Two numbers meet here and only one of them is drawn. How many places the
- * thread is anchored in belongs to the jump — it says what the button will do,
- * so it is said in the label and nowhere else. How many replies the thread has
- * collected is the count worth acting on, so that is what takes the pill. The
- * pill sits outside the button and carries its own word, so a drifted thread
- * with no jump left to offer still counts its replies out loud.
- */
-function Quote(
- { drifted, onSelect, places = 0, replies = 0, text }: {
- drifted?: boolean;
- onSelect?: () => void;
- /** How many spans of prose the thread is anchored in. */
- places?: number;
- /** Answers to the opening comment, which is not itself a reply. */
- replies?: number;
- text: string;
- },
-) {
+function DraftHeader({ onClose, showClose }: { onClose: () => void; showClose: boolean }) {
return (
-