@coframe-gtm/annotations 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -8
- package/src/README.md +4 -3
- package/src/api.ts +12 -0
- package/src/bundle.ts +11 -5
- package/src/inject/build.ts +0 -2
- package/src/inject/bundle-source.generated.ts +1 -1
- package/src/signal.ts +56 -0
- package/src/store.ts +2 -2
- package/src/ui/Composer.ts +166 -0
- package/src/ui/Cursors.ts +52 -0
- package/src/ui/Pins.ts +68 -0
- package/src/ui/ThreadPanel.ts +120 -0
- package/src/ui/Toolbar.ts +88 -0
- package/src/ui/dom.ts +95 -0
- package/src/ui/overlay.ts +155 -0
- package/src/ui/styles.ts +23 -0
- package/src/ui/App.tsx +0 -69
- package/src/ui/Composer.tsx +0 -138
- package/src/ui/Cursors.tsx +0 -50
- package/src/ui/Pins.tsx +0 -58
- package/src/ui/ThreadPanel.tsx +0 -124
- package/src/ui/Toolbar.tsx +0 -116
package/src/ui/Composer.tsx
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Composer popup — the comment + intent/severity form shown when a
|
|
3
|
-
* target is captured. Local form state lives in preact/hooks; on
|
|
4
|
-
* submit it pushes through the AFS API and clears the draft.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ComponentChild } from "preact";
|
|
8
|
-
import { useState } from "preact/hooks";
|
|
9
|
-
|
|
10
|
-
import { api } from "../api.js";
|
|
11
|
-
import type { CapturedTarget } from "../capture.js";
|
|
12
|
-
import { author, draft } from "../store.js";
|
|
13
|
-
import type {
|
|
14
|
-
AnnotationIntent,
|
|
15
|
-
AnnotationKind,
|
|
16
|
-
AnnotationSeverity,
|
|
17
|
-
} from "../types.js";
|
|
18
|
-
import {
|
|
19
|
-
INTENTS,
|
|
20
|
-
SEVERITIES,
|
|
21
|
-
SEVERITY_COLOR,
|
|
22
|
-
SEVERITY_NEUTRAL,
|
|
23
|
-
} from "./constants.js";
|
|
24
|
-
import { truncate } from "./helpers.js";
|
|
25
|
-
|
|
26
|
-
interface ChipProps {
|
|
27
|
-
label: string;
|
|
28
|
-
on: boolean;
|
|
29
|
-
color: string;
|
|
30
|
-
onClick: () => void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function Chip({ label, on, color, onClick }: ChipProps): ComponentChild {
|
|
34
|
-
return (
|
|
35
|
-
<button
|
|
36
|
-
class={`cf-chip${on ? " on" : ""}`}
|
|
37
|
-
style={on ? `--cf-chip:${color};` : undefined}
|
|
38
|
-
onClick={onClick}
|
|
39
|
-
>
|
|
40
|
-
{label}
|
|
41
|
-
</button>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface ComposerProps {
|
|
46
|
-
target: CapturedTarget;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function Composer({ target }: ComposerProps): ComponentChild {
|
|
50
|
-
const [comment, setComment] = useState("");
|
|
51
|
-
const [intent, setIntent] = useState<AnnotationIntent>("change");
|
|
52
|
-
const [severity, setSeverity] = useState<AnnotationSeverity>("important");
|
|
53
|
-
|
|
54
|
-
const submit = (): void => {
|
|
55
|
-
const text = comment.trim();
|
|
56
|
-
if (!text) return;
|
|
57
|
-
api.addAnnotation({
|
|
58
|
-
...target,
|
|
59
|
-
comment: text,
|
|
60
|
-
intent,
|
|
61
|
-
severity,
|
|
62
|
-
kind: "feedback" satisfies AnnotationKind,
|
|
63
|
-
author: author.value,
|
|
64
|
-
});
|
|
65
|
-
draft.value = null;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const cancel = (): void => {
|
|
69
|
-
draft.value = null;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const label = target.isMultiSelect
|
|
73
|
-
? `${target.elementBoundingBoxes?.length ?? 0} elements`
|
|
74
|
-
: target.selectedText
|
|
75
|
-
? `"${truncate(target.selectedText, 40)}"`
|
|
76
|
-
: `<${target.element}>`;
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div class="cf-popup cf-composer">
|
|
80
|
-
<div key="head" class="cf-popup-head">
|
|
81
|
-
<span key="t" class="cf-popup-target">{label}</span>
|
|
82
|
-
<button key="x" class="cf-icon-btn" onClick={cancel} title="Cancel">
|
|
83
|
-
✕
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
<textarea
|
|
87
|
-
key="ta"
|
|
88
|
-
class="cf-textarea"
|
|
89
|
-
placeholder="Describe the change… (Markdown supported)"
|
|
90
|
-
autofocus
|
|
91
|
-
value={comment}
|
|
92
|
-
onInput={(e: Event) => setComment((e.target as HTMLTextAreaElement).value)}
|
|
93
|
-
onKeyDown={(e: KeyboardEvent) => {
|
|
94
|
-
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") submit();
|
|
95
|
-
}}
|
|
96
|
-
/>
|
|
97
|
-
<div key="intent" class="cf-field">
|
|
98
|
-
<span key="l" class="cf-field-label">Intent</span>
|
|
99
|
-
<div key="g" class="cf-chipset">
|
|
100
|
-
{INTENTS.map((it) => (
|
|
101
|
-
<Chip
|
|
102
|
-
key={it}
|
|
103
|
-
label={it}
|
|
104
|
-
on={intent === it}
|
|
105
|
-
color={SEVERITY_NEUTRAL}
|
|
106
|
-
onClick={() => setIntent(it)}
|
|
107
|
-
/>
|
|
108
|
-
))}
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
<div key="sev" class="cf-field">
|
|
112
|
-
<span key="l" class="cf-field-label">Severity</span>
|
|
113
|
-
<div key="g" class="cf-chipset">
|
|
114
|
-
{SEVERITIES.map((s) => (
|
|
115
|
-
<Chip
|
|
116
|
-
key={s}
|
|
117
|
-
label={s}
|
|
118
|
-
on={severity === s}
|
|
119
|
-
color={SEVERITY_COLOR[s]}
|
|
120
|
-
onClick={() => setSeverity(s)}
|
|
121
|
-
/>
|
|
122
|
-
))}
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
<div key="actions" class="cf-popup-actions">
|
|
126
|
-
<button key="c" class="cf-btn" onClick={cancel}>Cancel</button>
|
|
127
|
-
<button
|
|
128
|
-
key="s"
|
|
129
|
-
class="cf-btn cf-btn-primary"
|
|
130
|
-
disabled={!comment.trim()}
|
|
131
|
-
onClick={submit}
|
|
132
|
-
>
|
|
133
|
-
Comment
|
|
134
|
-
</button>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
package/src/ui/Cursors.tsx
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live presence cursors — one labeled arrow per remote actor (human
|
|
3
|
-
* reviewers + spun-off agents) collaborating on the same page.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ComponentChild } from "preact";
|
|
7
|
-
|
|
8
|
-
import type { PresenceCursor } from "../store.js";
|
|
9
|
-
import { CURSOR_PALETTE } from "./constants.js";
|
|
10
|
-
import { hashId } from "./helpers.js";
|
|
11
|
-
|
|
12
|
-
interface CursorProps {
|
|
13
|
-
c: PresenceCursor;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function Cursor({ c }: CursorProps): ComponentChild {
|
|
17
|
-
// x is % of viewport width; y is px from document top.
|
|
18
|
-
const left = (c.x / 100) * window.innerWidth;
|
|
19
|
-
const top = c.y - window.scrollY;
|
|
20
|
-
const color =
|
|
21
|
-
c.color ??
|
|
22
|
-
(c.kind === "agent"
|
|
23
|
-
? CURSOR_PALETTE[Math.abs(hashId(c.id)) % CURSOR_PALETTE.length]!
|
|
24
|
-
: "#ffffff");
|
|
25
|
-
return (
|
|
26
|
-
<div
|
|
27
|
-
class={`cf-cursor cf-cursor-${c.kind}`}
|
|
28
|
-
style={`top:${top}px;left:${left}px;--cf-cursor:${color};`}
|
|
29
|
-
>
|
|
30
|
-
{/* Arrow pointer. */}
|
|
31
|
-
<svg
|
|
32
|
-
key="arrow"
|
|
33
|
-
width="18"
|
|
34
|
-
height="18"
|
|
35
|
-
viewBox="0 0 18 18"
|
|
36
|
-
class="cf-cursor-arrow"
|
|
37
|
-
>
|
|
38
|
-
<path
|
|
39
|
-
d="M2 2 L2 14 L6 10 L9 16 L11 15 L8 9 L14 9 Z"
|
|
40
|
-
fill={color}
|
|
41
|
-
stroke="rgba(0,0,0,0.35)"
|
|
42
|
-
stroke-width="0.75"
|
|
43
|
-
/>
|
|
44
|
-
</svg>
|
|
45
|
-
<span key="label" class="cf-cursor-label">
|
|
46
|
-
{`${c.kind === "agent" ? "🤖 " : ""}${c.label}`}
|
|
47
|
-
</span>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
|
-
}
|
package/src/ui/Pins.tsx
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pins + multi-select boxes.
|
|
3
|
-
*
|
|
4
|
-
* - `Pin` — one numbered marker per annotation, re-anchored to
|
|
5
|
-
* its live element each render.
|
|
6
|
-
* - `MultiBox` — a pulsing outline around each element in the
|
|
7
|
-
* current multi-selection set.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ComponentChild } from "preact";
|
|
11
|
-
|
|
12
|
-
import { activeThreadId } from "../store.js";
|
|
13
|
-
import type { Annotation } from "../types.js";
|
|
14
|
-
import { DEFAULT_DOT, SEVERITY_COLOR } from "./constants.js";
|
|
15
|
-
import { boxStyle, firstLine, pinPosition } from "./helpers.js";
|
|
16
|
-
|
|
17
|
-
interface PinProps {
|
|
18
|
-
a: Annotation;
|
|
19
|
-
index: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function Pin({ a, index }: PinProps): ComponentChild {
|
|
23
|
-
const pos = pinPosition(a);
|
|
24
|
-
if (!pos) return null;
|
|
25
|
-
const color = a.severity ? SEVERITY_COLOR[a.severity] : DEFAULT_DOT;
|
|
26
|
-
const resolved = a.status === "resolved" || a.status === "dismissed";
|
|
27
|
-
return (
|
|
28
|
-
<button
|
|
29
|
-
class={`cf-pin${resolved ? " resolved" : ""}${a.author?.kind === "agent" ? " agent" : ""}`}
|
|
30
|
-
style={`top:${pos.top}px;left:${pos.left}px;--cf-pin:${color};`}
|
|
31
|
-
title={firstLine(a.comment)}
|
|
32
|
-
onClick={() => {
|
|
33
|
-
activeThreadId.value = activeThreadId.value === a.id ? null : a.id;
|
|
34
|
-
}}
|
|
35
|
-
>
|
|
36
|
-
{a.author?.kind === "agent" ? "★" : String(index)}
|
|
37
|
-
</button>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface MultiBoxProps {
|
|
42
|
-
el: Element;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function MultiBox({ el }: MultiBoxProps): ComponentChild {
|
|
46
|
-
const rect = el.getBoundingClientRect();
|
|
47
|
-
return (
|
|
48
|
-
<div
|
|
49
|
-
class="cf-multi-box"
|
|
50
|
-
style={boxStyle({
|
|
51
|
-
top: rect.top,
|
|
52
|
-
left: rect.left,
|
|
53
|
-
width: rect.width,
|
|
54
|
-
height: rect.height,
|
|
55
|
-
})}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
}
|
package/src/ui/ThreadPanel.tsx
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thread panel — the per-pin discussion (human ↔ agent). Shows the
|
|
3
|
-
* root annotation comment plus the reply thread, with a compose box
|
|
4
|
-
* and a resolve affordance. Reads the active annotation from the
|
|
5
|
-
* `../store.js` signals by id.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ComponentChild } from "preact";
|
|
9
|
-
import { useState } from "preact/hooks";
|
|
10
|
-
|
|
11
|
-
import { api } from "../api.js";
|
|
12
|
-
import { activeThreadId, annotations, author } from "../store.js";
|
|
13
|
-
import { SEVERITY_COLOR } from "./constants.js";
|
|
14
|
-
|
|
15
|
-
interface MessageProps {
|
|
16
|
-
role: "human" | "agent";
|
|
17
|
-
name: string;
|
|
18
|
-
content: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function Message({ role, name, content }: MessageProps): ComponentChild {
|
|
22
|
-
return (
|
|
23
|
-
<div class={`cf-msg cf-msg-${role}`}>
|
|
24
|
-
<div key="meta" class="cf-msg-meta">
|
|
25
|
-
<span key="av" class={`cf-avatar cf-avatar-${role}`}>
|
|
26
|
-
{role === "agent" ? "🤖" : "🧑"}
|
|
27
|
-
</span>
|
|
28
|
-
<span key="n" class="cf-msg-name">{name}</span>
|
|
29
|
-
</div>
|
|
30
|
-
<div key="c" class="cf-msg-body">{content}</div>
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface ThreadPanelProps {
|
|
36
|
-
id: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function ThreadPanel({ id }: ThreadPanelProps): ComponentChild {
|
|
40
|
-
const [reply, setReply] = useState("");
|
|
41
|
-
const a = annotations.value.find((x) => x.id === id);
|
|
42
|
-
if (!a) return null;
|
|
43
|
-
|
|
44
|
-
const send = (): void => {
|
|
45
|
-
const text = reply.trim();
|
|
46
|
-
if (!text) return;
|
|
47
|
-
api.replyToAnnotation(id, { role: author.value.kind, content: text });
|
|
48
|
-
setReply("");
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div class="cf-popup cf-thread">
|
|
53
|
-
<div key="head" class="cf-popup-head">
|
|
54
|
-
<span key="t" class="cf-popup-target">{`<${a.element}>`}</span>
|
|
55
|
-
{a.severity ? (
|
|
56
|
-
<span
|
|
57
|
-
key="sev"
|
|
58
|
-
class="cf-tag"
|
|
59
|
-
style={`--cf-tag:${SEVERITY_COLOR[a.severity]};`}
|
|
60
|
-
>
|
|
61
|
-
{a.severity}
|
|
62
|
-
</span>
|
|
63
|
-
) : null}
|
|
64
|
-
<button
|
|
65
|
-
key="x"
|
|
66
|
-
class="cf-icon-btn"
|
|
67
|
-
onClick={() => (activeThreadId.value = null)}
|
|
68
|
-
title="Close"
|
|
69
|
-
>
|
|
70
|
-
✕
|
|
71
|
-
</button>
|
|
72
|
-
</div>
|
|
73
|
-
<div key="body" class="cf-thread-body">
|
|
74
|
-
<Message
|
|
75
|
-
key="root"
|
|
76
|
-
role={a.author?.kind ?? "human"}
|
|
77
|
-
name={a.author?.displayName ?? "You"}
|
|
78
|
-
content={a.comment}
|
|
79
|
-
/>
|
|
80
|
-
{(a.thread ?? []).map((m) => (
|
|
81
|
-
<Message
|
|
82
|
-
key={m.id}
|
|
83
|
-
role={m.role}
|
|
84
|
-
name={m.role === "agent" ? "Agent" : "You"}
|
|
85
|
-
content={m.content}
|
|
86
|
-
/>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
<div key="compose" class="cf-thread-compose">
|
|
90
|
-
<textarea
|
|
91
|
-
key="ta"
|
|
92
|
-
class="cf-textarea cf-textarea-sm"
|
|
93
|
-
placeholder="Reply…"
|
|
94
|
-
value={reply}
|
|
95
|
-
onInput={(e: Event) => setReply((e.target as HTMLTextAreaElement).value)}
|
|
96
|
-
onKeyDown={(e: KeyboardEvent) => {
|
|
97
|
-
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") send();
|
|
98
|
-
}}
|
|
99
|
-
/>
|
|
100
|
-
<div key="row" class="cf-thread-actions">
|
|
101
|
-
{a.status !== "resolved" ? (
|
|
102
|
-
<button
|
|
103
|
-
key="resolve"
|
|
104
|
-
class="cf-btn cf-btn-ghost"
|
|
105
|
-
onClick={() => api.resolveAnnotation(id, author.value.kind)}
|
|
106
|
-
>
|
|
107
|
-
Resolve
|
|
108
|
-
</button>
|
|
109
|
-
) : (
|
|
110
|
-
<span key="resolved" class="cf-resolved-tag">✓ resolved</span>
|
|
111
|
-
)}
|
|
112
|
-
<button
|
|
113
|
-
key="send"
|
|
114
|
-
class="cf-btn cf-btn-primary"
|
|
115
|
-
disabled={!reply.trim()}
|
|
116
|
-
onClick={send}
|
|
117
|
-
>
|
|
118
|
-
Reply
|
|
119
|
-
</button>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
);
|
|
124
|
-
}
|
package/src/ui/Toolbar.tsx
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Toolbar — mode + tool switches, annotation count, and a contextual
|
|
3
|
-
* hint. Reads cross-cutting state from the `../store.js` signals and
|
|
4
|
-
* commits multi-selections through `../picker.js`.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ComponentChild } from "preact";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
annotations,
|
|
11
|
-
feedbackTool,
|
|
12
|
-
hoverBox,
|
|
13
|
-
mode,
|
|
14
|
-
multiSelection,
|
|
15
|
-
} from "../store.js";
|
|
16
|
-
import { commitMultiSelection } from "../picker.js";
|
|
17
|
-
|
|
18
|
-
export function hintFor(tool: string): string {
|
|
19
|
-
if (tool === "text") return "Select text on the page to annotate it.";
|
|
20
|
-
if (tool === "multi") return "Click elements to group them, then commit.";
|
|
21
|
-
return "Click any element to comment. Esc to exit.";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface SegButtonProps {
|
|
25
|
-
label: string;
|
|
26
|
-
on: boolean;
|
|
27
|
-
onClick: () => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function SegButton({ label, on, onClick }: SegButtonProps): ComponentChild {
|
|
31
|
-
return (
|
|
32
|
-
<button class={`cf-seg-btn${on ? " on" : ""}`} onClick={onClick}>
|
|
33
|
-
{label}
|
|
34
|
-
</button>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function Toolbar(): ComponentChild {
|
|
39
|
-
const active = mode.value === "feedback";
|
|
40
|
-
const count = annotations.value.length;
|
|
41
|
-
const multi = multiSelection.value.length;
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div key="toolbar" class="cf-toolbar">
|
|
45
|
-
<div key="head" class="cf-toolbar-head">
|
|
46
|
-
<span key="dot" class="cf-logo-dot" />
|
|
47
|
-
<span key="label">Annotations</span>
|
|
48
|
-
<span key="count" class="cf-count">{String(count)}</span>
|
|
49
|
-
</div>
|
|
50
|
-
<div key="modes" class="cf-seg">
|
|
51
|
-
<SegButton
|
|
52
|
-
key="View"
|
|
53
|
-
label="View"
|
|
54
|
-
on={mode.value === "view"}
|
|
55
|
-
onClick={() => {
|
|
56
|
-
mode.value = "view";
|
|
57
|
-
hoverBox.value = null;
|
|
58
|
-
multiSelection.value = [];
|
|
59
|
-
}}
|
|
60
|
-
/>
|
|
61
|
-
<SegButton
|
|
62
|
-
key="Comment"
|
|
63
|
-
label="Comment"
|
|
64
|
-
on={active}
|
|
65
|
-
onClick={() => {
|
|
66
|
-
mode.value = "feedback";
|
|
67
|
-
}}
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
|
-
{active ? (
|
|
71
|
-
<div key="tools" class="cf-seg cf-tools">
|
|
72
|
-
<SegButton
|
|
73
|
-
key="Element"
|
|
74
|
-
label="Element"
|
|
75
|
-
on={feedbackTool.value === "element"}
|
|
76
|
-
onClick={() => {
|
|
77
|
-
feedbackTool.value = "element";
|
|
78
|
-
multiSelection.value = [];
|
|
79
|
-
}}
|
|
80
|
-
/>
|
|
81
|
-
<SegButton
|
|
82
|
-
key="Text"
|
|
83
|
-
label="Text"
|
|
84
|
-
on={feedbackTool.value === "text"}
|
|
85
|
-
onClick={() => {
|
|
86
|
-
feedbackTool.value = "text";
|
|
87
|
-
multiSelection.value = [];
|
|
88
|
-
}}
|
|
89
|
-
/>
|
|
90
|
-
<SegButton
|
|
91
|
-
key="Multi"
|
|
92
|
-
label="Multi"
|
|
93
|
-
on={feedbackTool.value === "multi"}
|
|
94
|
-
onClick={() => {
|
|
95
|
-
feedbackTool.value = "multi";
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
</div>
|
|
99
|
-
) : null}
|
|
100
|
-
{active && feedbackTool.value === "multi" && multi ? (
|
|
101
|
-
<button
|
|
102
|
-
key="commit"
|
|
103
|
-
class="cf-btn cf-btn-primary cf-commit"
|
|
104
|
-
onClick={commitMultiSelection}
|
|
105
|
-
>
|
|
106
|
-
{`Comment ${multi} element${multi === 1 ? "" : "s"}`}
|
|
107
|
-
</button>
|
|
108
|
-
) : null}
|
|
109
|
-
{active ? (
|
|
110
|
-
<div key="hint" class="cf-hint">
|
|
111
|
-
{hintFor(feedbackTool.value)}
|
|
112
|
-
</div>
|
|
113
|
-
) : null}
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
}
|