@coframe-gtm/annotations 1.0.4 → 1.2.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/src/ui/styles.ts CHANGED
@@ -6,6 +6,12 @@
6
6
  */
7
7
 
8
8
  export const STYLES = `
9
+ /* Box-sizing reset — the host page's reset is scoped to its own tree and
10
+ doesn't reach the overlay, so width:100% controls + padding would
11
+ otherwise overflow their popups. Keep everything border-box. */
12
+ .cf-overlay, .cf-overlay *, .cf-overlay *::before, .cf-overlay *::after {
13
+ box-sizing: border-box;
14
+ }
9
15
  .cf-overlay {
10
16
  position: fixed;
11
17
  inset: 0;
@@ -17,12 +23,18 @@ export const STYLES = `
17
23
  --cf-border: rgba(255, 255, 255, 0.1);
18
24
  --cf-muted: rgba(255, 255, 255, 0.55);
19
25
  --cf-accent: #a78bfa;
26
+ /* Liquid-glass fill for popups/panels — low alpha so the backdrop blur
27
+ actually reads as frosted glass instead of a solid black box. */
28
+ --cf-glass: rgba(24, 24, 34, 0.55);
29
+ --cf-glass-border: rgba(255, 255, 255, 0.14);
20
30
  }
21
31
  .cf-overlay[data-theme="light"] {
22
32
  color: #18181b;
23
33
  --cf-bg: rgba(252, 252, 253, 0.97);
24
34
  --cf-border: rgba(0, 0, 0, 0.1);
25
35
  --cf-muted: rgba(0, 0, 0, 0.5);
36
+ --cf-glass: rgba(252, 252, 253, 0.60);
37
+ --cf-glass-border: rgba(0, 0, 0, 0.10);
26
38
  }
27
39
 
28
40
  /* Hover highlight + multi-select boxes */
@@ -77,8 +89,29 @@ export const STYLES = `
77
89
  .cf-cursor-agent .cf-cursor-label { color: #0b0b0f; }
78
90
 
79
91
  /* Pins */
80
- .cf-pin {
92
+ /* Outline box hugging the annotated element (so it's obvious what the
93
+ annotation refers to). Colour-coded per author via --cf-pin. */
94
+ /* A multi-element annotation renders several .cf-anno outlines wrapped in
95
+ one group — display:contents so the wrapper itself adds no layout box and
96
+ the fixed-positioned outlines anchor straight to the viewport. */
97
+ .cf-anno-group { display: contents; }
98
+ .cf-anno {
81
99
  position: fixed;
100
+ pointer-events: none;
101
+ border: 1.5px solid var(--cf-pin, #a78bfa);
102
+ border-radius: 7px;
103
+ background: color-mix(in srgb, var(--cf-pin, #a78bfa) 9%, transparent);
104
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--cf-pin, #a78bfa) 15%, transparent);
105
+ z-index: 2;
106
+ }
107
+ /* Entrance animation plays only on first render (.cf-fresh), never on the
108
+ scroll/resize re-renders — otherwise the box flickers while scrolling. */
109
+ .cf-anno.cf-fresh { animation: cf-rise .18s cubic-bezier(.16,1,.3,1) both; }
110
+ .cf-anno.agent { border-style: dashed; }
111
+ .cf-anno.resolved { opacity: 0.4; }
112
+ .cf-pin {
113
+ position: absolute;
114
+ top: -11px; left: -11px;
82
115
  width: 22px; height: 22px;
83
116
  border-radius: 50% 50% 50% 2px;
84
117
  background: var(--cf-pin, #a78bfa);
@@ -92,21 +125,40 @@ export const STYLES = `
92
125
  z-index: 3;
93
126
  transition: transform .12s cubic-bezier(.34,1.56,.64,1);
94
127
  padding: 0;
95
- /* Pop in when a pin first renders so new annotations are obvious. */
96
- animation: cf-pin-pop .32s cubic-bezier(.34,1.56,.64,1) both;
97
128
  }
129
+ /* Pop in only when a pin first renders (.cf-fresh) — not on the scroll
130
+ re-renders, which would otherwise replay the pop on every tick. */
131
+ .cf-pin.cf-fresh { animation: cf-pin-pop .32s cubic-bezier(.34,1.56,.64,1) both; }
98
132
  .cf-pin:hover { transform: scale(1.2); }
99
133
  .cf-pin.resolved { opacity: 0.45; }
100
- /* Agent-pushed pins pulse a ring so reviewers spot what the agent added. */
101
- .cf-pin.agent { border-style: dashed; }
102
- .cf-pin.agent::after {
103
- content: "";
104
- position: absolute; inset: -4px;
105
- border-radius: inherit;
106
- border: 2px solid var(--cf-pin, #a78bfa);
107
- animation: cf-pin-ring 1.6s ease-out 3;
134
+ /* Human vs agent reads from the box outline (solid vs dashed) + the pin
135
+ glyph (number vs initial) + colour — no extra per-pin chrome. */
136
+
137
+ /* Hover-to-preview card — shown next to a pin on mouseenter. */
138
+ .cf-pin-preview {
139
+ position: absolute;
140
+ top: 26px; left: 0;
141
+ max-width: 240px;
142
+ width: max-content;
108
143
  pointer-events: none;
109
- }
144
+ background: var(--cf-glass);
145
+ border: 1px solid var(--cf-glass-border);
146
+ border-radius: 9px;
147
+ padding: 7px 10px;
148
+ font: 500 12px -apple-system, system-ui;
149
+ line-height: 1.4;
150
+ color: #f4f4f5;
151
+ white-space: normal;
152
+ overflow-wrap: anywhere;
153
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
154
+ backdrop-filter: blur(24px) saturate(180%);
155
+ box-shadow:
156
+ 0 8px 28px rgba(0,0,0,0.4),
157
+ inset 0 1px 0 rgba(255,255,255,0.18);
158
+ z-index: 7;
159
+ animation: cf-rise .14s cubic-bezier(.16,1,.3,1) both;
160
+ }
161
+ .cf-overlay[data-theme="light"] .cf-pin-preview { color: #18181b; }
110
162
 
111
163
  @keyframes cf-pin-pop {
112
164
  0% { transform: scale(0); opacity: 0; }
@@ -123,24 +175,83 @@ export const STYLES = `
123
175
  position: fixed;
124
176
  left: 16px; bottom: 16px;
125
177
  pointer-events: auto;
126
- background: var(--cf-bg);
127
- border: 1px solid var(--cf-border);
128
- border-radius: 14px;
129
- padding: 10px;
178
+ /* Liquid glass — translucent, frosted, with a bright top edge. */
179
+ background: rgba(24,24,34,0.52);
180
+ border: 1px solid rgba(255,255,255,0.14);
181
+ border-radius: 17px;
182
+ padding: 9px;
130
183
  display: flex; flex-direction: column; gap: 8px;
131
- backdrop-filter: blur(16px) saturate(140%);
132
- box-shadow: 0 16px 48px rgba(0,0,0,0.5);
133
- min-width: 220px;
184
+ -webkit-backdrop-filter: blur(30px) saturate(185%);
185
+ backdrop-filter: blur(30px) saturate(185%);
186
+ box-shadow:
187
+ 0 18px 50px rgba(0,0,0,0.34),
188
+ inset 0 1px 0 rgba(255,255,255,0.22),
189
+ inset 0 -1px 0 rgba(0,0,0,0.22);
134
190
  z-index: 4;
135
191
  }
136
192
  .cf-toolbar-head {
137
193
  display: flex; align-items: center; gap: 7px;
138
- font-weight: 700; letter-spacing: 0.02em;
194
+ cursor: grab; user-select: none; touch-action: none;
195
+ }
196
+ .cf-toolbar-head:active { cursor: grabbing; }
197
+ /* Visible drag affordance — signals the panel can be moved. */
198
+ .cf-grip {
199
+ flex: 0 0 auto; color: var(--cf-muted);
200
+ font-size: 13px; line-height: 1; letter-spacing: -2px; opacity: 0.5;
201
+ }
202
+ .cf-toolbar-head:hover .cf-grip { opacity: 0.85; }
203
+ /* Logo button — tap to collapse back to the puck. */
204
+ .cf-collapse {
205
+ display: inline-flex; align-items: center; justify-content: center;
206
+ width: 26px; height: 26px; flex: 0 0 auto; padding: 0;
207
+ border: 0; border-radius: 8px; cursor: pointer;
208
+ /* Light tile so a dark brand logo reads on the dark panel. */
209
+ background: #fff;
210
+ box-shadow: 0 1px 3px rgba(0,0,0,0.25);
211
+ }
212
+ .cf-collapse:hover { filter: brightness(0.94); }
213
+ .cf-brand {
214
+ display: inline-flex; align-items: center; justify-content: center;
215
+ width: 22px; height: 22px; flex: 0 0 auto; pointer-events: none;
216
+ }
217
+ .cf-brand svg { width: 18px; height: 18px; display: block; }
218
+ .cf-toolbar.cf-dragging {
219
+ box-shadow: 0 24px 64px rgba(0,0,0,0.6);
220
+ transition: none;
139
221
  }
140
222
  .cf-logo-dot {
141
- width: 8px; height: 8px; border-radius: 50%;
142
- background: var(--cf-accent);
143
- box-shadow: 0 0 10px var(--cf-accent);
223
+ width: 9px; height: 9px; border-radius: 50%;
224
+ background: var(--cf-accent); box-shadow: 0 0 10px var(--cf-accent);
225
+ }
226
+
227
+ /* Collapsed FAB — a draggable logo puck; tap to expand. */
228
+ .cf-fab {
229
+ position: fixed; left: 16px; bottom: 16px;
230
+ width: 48px; height: 48px; border-radius: 16px;
231
+ pointer-events: auto; cursor: grab; user-select: none; touch-action: none;
232
+ display: flex; align-items: center; justify-content: center;
233
+ /* Liquid glass puck — frosted light so the dark mark reads. */
234
+ background: rgba(255,255,255,0.66);
235
+ -webkit-backdrop-filter: blur(22px) saturate(185%);
236
+ backdrop-filter: blur(22px) saturate(185%);
237
+ box-shadow:
238
+ 0 12px 34px rgba(0,0,0,0.20),
239
+ inset 0 1px 0 rgba(255,255,255,0.95),
240
+ 0 0 0 0.5px rgba(0,0,0,0.06);
241
+ z-index: 4;
242
+ transition: transform .13s cubic-bezier(.34,1.56,.64,1), box-shadow .15s ease;
243
+ }
244
+ .cf-fab:hover { transform: scale(1.06); }
245
+ .cf-fab.cf-dragging { cursor: grabbing; transition: none; box-shadow: 0 18px 44px rgba(0,0,0,0.42); }
246
+ .cf-fab .cf-brand { width: 28px; height: 28px; cursor: inherit; }
247
+ .cf-fab .cf-brand svg { width: 24px; height: 24px; }
248
+ .cf-fab-badge {
249
+ position: absolute; top: -5px; right: -5px;
250
+ min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box;
251
+ border-radius: 9px; background: var(--cf-accent); color: #fff;
252
+ font: 800 10px -apple-system, system-ui;
253
+ display: flex; align-items: center; justify-content: center;
254
+ box-shadow: 0 0 0 2px var(--cf-bg);
144
255
  }
145
256
  .cf-count {
146
257
  margin-left: auto;
@@ -151,24 +262,45 @@ export const STYLES = `
151
262
  }
152
263
  .cf-overlay[data-theme="light"] .cf-count { background: rgba(0,0,0,0.06); }
153
264
 
265
+ /* View / Comment reads as a pill switch — rounded track + sliding pill. */
154
266
  .cf-seg {
155
267
  display: flex; gap: 2px;
156
- background: rgba(255,255,255,0.06);
157
- border-radius: 9px; padding: 2px;
268
+ background: rgba(255,255,255,0.07);
269
+ border-radius: 999px; padding: 3px;
158
270
  }
159
271
  .cf-overlay[data-theme="light"] .cf-seg { background: rgba(0,0,0,0.05); }
160
272
  .cf-seg-btn {
161
273
  flex: 1; border: none; background: transparent; color: var(--cf-muted);
162
- padding: 5px 10px; border-radius: 7px; font: 600 12px -apple-system, system-ui;
274
+ padding: 5px 14px; border-radius: 999px; cursor: pointer;
275
+ font: 600 12px -apple-system, system-ui;
276
+ transition: color .15s ease, background .2s ease;
163
277
  }
278
+ .cf-seg-btn:hover { color: #fff; }
164
279
  .cf-seg-btn.on {
165
280
  background: var(--cf-accent); color: #fff;
166
- box-shadow: 0 1px 4px rgba(0,0,0,0.3);
281
+ box-shadow: 0 1px 5px rgba(124,92,255,0.45);
282
+ }
283
+ /* Suggestions navigator — count + jump to each annotation. */
284
+ .cf-nav {
285
+ display: flex; align-items: center; justify-content: space-between; gap: 6px;
286
+ background: rgba(255,255,255,0.05); border-radius: 10px; padding: 2px 3px;
167
287
  }
288
+ .cf-nav-btn {
289
+ border: 0; background: transparent; color: var(--cf-muted); cursor: pointer;
290
+ width: 24px; height: 22px; border-radius: 7px; font-size: 16px; line-height: 1; padding: 0;
291
+ }
292
+ .cf-nav-btn:hover { background: rgba(255,255,255,0.1); color: #fff; }
293
+ .cf-nav-label { font: 700 11px -apple-system, system-ui; color: var(--cf-muted); }
168
294
  .cf-tools .cf-seg-btn.on { background: rgba(255,255,255,0.16); color: #fff; }
169
295
  .cf-overlay[data-theme="light"] .cf-tools .cf-seg-btn.on { background: rgba(0,0,0,0.12); color: #111; }
170
296
  .cf-hint { font-size: 11px; color: var(--cf-muted); padding: 0 2px; }
171
297
  .cf-commit { width: 100%; }
298
+ .cf-launch {
299
+ width: 100%; border: 0; margin-top: 2px;
300
+ background: linear-gradient(135deg, #7c5cff, #3ecf8e);
301
+ box-shadow: 0 6px 18px rgba(124,92,255,0.35);
302
+ }
303
+ .cf-launch:hover { filter: brightness(1.07); }
172
304
 
173
305
  /* Buttons */
174
306
  .cf-btn {
@@ -190,13 +322,20 @@ export const STYLES = `
190
322
  left: 16px; bottom: 92px;
191
323
  width: 320px;
192
324
  pointer-events: auto;
193
- background: var(--cf-bg);
194
- border: 1px solid var(--cf-border);
195
- border-radius: 14px;
325
+ /* Liquid glass — translucent + heavily frosted with a bright top edge,
326
+ matching the toolbar/FAB. The low-alpha fill is what lets the blur
327
+ read as glass rather than a solid black box. */
328
+ background: var(--cf-glass);
329
+ border: 1px solid var(--cf-glass-border);
330
+ border-radius: 16px;
196
331
  padding: 12px;
197
332
  display: flex; flex-direction: column; gap: 10px;
198
- backdrop-filter: blur(16px) saturate(140%);
199
- box-shadow: 0 16px 48px rgba(0,0,0,0.55);
333
+ -webkit-backdrop-filter: blur(30px) saturate(185%);
334
+ backdrop-filter: blur(30px) saturate(185%);
335
+ box-shadow:
336
+ 0 18px 50px rgba(0,0,0,0.40),
337
+ inset 0 1px 0 rgba(255,255,255,0.22),
338
+ inset 0 -1px 0 rgba(0,0,0,0.22);
200
339
  z-index: 5;
201
340
  animation: cf-rise .2s cubic-bezier(.16,1,.3,1) both;
202
341
  }
@@ -218,6 +357,13 @@ export const STYLES = `
218
357
  width: 22px; height: 22px; border-radius: 6px;
219
358
  }
220
359
  .cf-icon-btn:hover { background: rgba(255,255,255,0.08); }
360
+ /* Clear / erase-board — small text button after the count. */
361
+ .cf-clear {
362
+ margin-left: 4px; width: auto; height: auto; padding: 3px 8px;
363
+ font: 700 10px -apple-system, system-ui; text-transform: uppercase;
364
+ letter-spacing: 0.05em;
365
+ }
366
+ .cf-clear:hover { color: #fff; }
221
367
 
222
368
  .cf-textarea {
223
369
  width: 100%; min-height: 72px; resize: vertical;
@@ -246,7 +392,29 @@ export const STYLES = `
246
392
  border-color: var(--cf-chip, var(--cf-accent));
247
393
  color: #fff;
248
394
  }
249
- .cf-popup-actions { display: flex; gap: 8px; justify-content: flex-end; }
395
+ .cf-popup-actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; }
396
+
397
+ /* Composer grows out of the floater — the WAAPI morph drives it, so drop
398
+ the CSS keyframe to avoid a double animation. */
399
+ .cf-popup.cf-morph { animation: none; }
400
+ /* The logo in the popup head — the floater mark that flew over + opened. */
401
+ .cf-popup-brand {
402
+ width: 20px; height: 20px; flex: 0 0 auto;
403
+ display: inline-flex; align-items: center; justify-content: center;
404
+ background: #fff; border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,0.25);
405
+ }
406
+ .cf-popup-brand svg { width: 15px; height: 15px; display: block; }
407
+ /* Intent/severity disclosure — minimal by default. */
408
+ .cf-details { display: flex; flex-direction: column; gap: 10px; }
409
+ .cf-details[hidden] { display: none; }
410
+ .cf-disclose {
411
+ margin-right: auto; border: 0; background: transparent; color: var(--cf-muted);
412
+ font: 600 11px -apple-system, system-ui; cursor: pointer;
413
+ padding: 6px 6px; border-radius: 7px;
414
+ }
415
+ .cf-disclose:hover, .cf-disclose.on { color: #fff; background: rgba(255,255,255,0.06); }
416
+ .cf-overlay[data-theme="light"] .cf-disclose:hover,
417
+ .cf-overlay[data-theme="light"] .cf-disclose.on { color: #111; background: rgba(0,0,0,0.05); }
250
418
 
251
419
  /* Thread */
252
420
  .cf-thread-body {
@@ -255,7 +423,17 @@ export const STYLES = `
255
423
  }
256
424
  .cf-msg { display: flex; flex-direction: column; gap: 3px; }
257
425
  .cf-msg-meta { display: flex; align-items: center; gap: 6px; }
258
- .cf-avatar { font-size: 12px; }
426
+ .cf-avatar {
427
+ width: 20px; height: 20px; flex: 0 0 auto;
428
+ display: inline-flex; align-items: center; justify-content: center;
429
+ border-radius: 50%; overflow: hidden;
430
+ font: 800 9px -apple-system, system-ui; letter-spacing: 0.02em;
431
+ color: #fff; text-shadow: 0 1px 1px rgba(0,0,0,0.25);
432
+ box-shadow: 0 0 0 1.5px rgba(255,255,255,0.14), 0 1px 2px rgba(0,0,0,0.3);
433
+ }
434
+ /* Agents read as a distinct rounded "logo" tile; humans stay round. */
435
+ .cf-avatar-agent { border-radius: 7px; }
436
+ .cf-avatar-img { width: 100%; height: 100%; object-fit: cover; }
259
437
  .cf-msg-name {
260
438
  font: 700 11px -apple-system, system-ui; color: var(--cf-muted);
261
439
  }
package/src/ui/App.tsx DELETED
@@ -1,69 +0,0 @@
1
- /**
2
- * Root Preact tree — the full annotation overlay.
3
- *
4
- * Real JSX (the file is `.tsx`); esbuild + tsc transpile via the
5
- * package's `jsx: react-jsx` + `jsxImportSource: preact` config.
6
- * Local component state uses preact/hooks; cross-cutting state lives
7
- * in the signals from `../store.js`.
8
- *
9
- * Layers (all inside the closed Shadow DOM, fixed-positioned):
10
- * 1. hover highlight — element under the cursor in feedback mode
11
- * 2. multi-select boxes
12
- * 3. pins — one numbered marker per annotation
13
- * 4. toolbar — mode + tool switches
14
- * 5. composer popup — comment + intent/severity/kind on capture
15
- * 6. thread panel — per-pin discussion (human ↔ agent)
16
- */
17
-
18
- import type { ComponentChild } from "preact";
19
-
20
- import {
21
- activeThreadId,
22
- annotations,
23
- cursors,
24
- draft,
25
- hoverBox,
26
- multiSelection,
27
- theme,
28
- viewportTick,
29
- } from "../store.js";
30
- import { boxStyle } from "./helpers.js";
31
- import { Toolbar } from "./Toolbar.js";
32
- import { MultiBox, Pin } from "./Pins.js";
33
- import { Cursor } from "./Cursors.js";
34
- import { Composer } from "./Composer.js";
35
- import { ThreadPanel } from "./ThreadPanel.js";
36
- import { STYLES } from "./styles.js";
37
-
38
- export function App(): ComponentChild {
39
- const themeAttr = theme.value === "auto" ? "dark" : theme.value;
40
- // Subscribe to viewport changes so pins recompute their screen pos.
41
- void viewportTick.value;
42
-
43
- return (
44
- <div class="cf-overlay" data-theme={themeAttr}>
45
- <style key="styles">{STYLES}</style>
46
- {hoverBox.value && !draft.value ? (
47
- <div key="hover" class="cf-hover" style={boxStyle(hoverBox.value)} />
48
- ) : null}
49
- {multiSelection.value.map((el, i) => (
50
- <MultiBox key={`multi-${i}`} el={el} />
51
- ))}
52
- <div key="pins">
53
- {annotations.value.map((a, i) => (
54
- <Pin key={a.id} a={a} index={i + 1} />
55
- ))}
56
- </div>
57
- <div key="cursors">
58
- {cursors.value.map((c) => (
59
- <Cursor key={`cursor-${c.id}`} c={c} />
60
- ))}
61
- </div>
62
- <Toolbar />
63
- {draft.value ? <Composer key="composer" target={draft.value} /> : null}
64
- {activeThreadId.value ? (
65
- <ThreadPanel key="thread" id={activeThreadId.value} />
66
- ) : null}
67
- </div>
68
- );
69
- }
@@ -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
- }
@@ -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
- }