visual-comments 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +477 -0
- package/dist/core/aim.d.ts +21 -0
- package/dist/core/aim.js +21 -0
- package/dist/core/api.d.ts +5 -0
- package/dist/core/api.js +11 -0
- package/dist/core/metaball.d.ts +15 -0
- package/dist/core/metaball.js +42 -0
- package/dist/core/presence.d.ts +13 -0
- package/dist/core/presence.js +25 -0
- package/dist/core/surfaces.d.ts +95 -0
- package/dist/core/surfaces.js +197 -0
- package/dist/core/target.d.ts +59 -0
- package/dist/core/target.js +382 -0
- package/dist/core/threads.svelte.d.ts +80 -0
- package/dist/core/threads.svelte.js +336 -0
- package/dist/core/time.d.ts +3 -0
- package/dist/core/time.js +25 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/server/env.d.ts +78 -0
- package/dist/server/env.js +94 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.js +74 -0
- package/dist/server/variables.d.ts +28 -0
- package/dist/server/variables.js +73 -0
- package/dist/shared/limits.d.ts +9 -0
- package/dist/shared/limits.js +10 -0
- package/dist/shared/protocol.d.ts +6 -0
- package/dist/shared/protocol.js +7 -0
- package/dist/shared/types.d.ts +157 -0
- package/dist/shared/types.js +2 -0
- package/dist/sveltekit/CommentTools.svelte +98 -0
- package/dist/sveltekit/CommentTools.svelte.d.ts +6 -0
- package/dist/sveltekit/Comments.svelte +104 -0
- package/dist/sveltekit/Comments.svelte.d.ts +25 -0
- package/dist/sveltekit/Toolbar.svelte +64 -0
- package/dist/sveltekit/Toolbar.svelte.d.ts +13 -0
- package/dist/sveltekit/ToolbarButton.svelte +66 -0
- package/dist/sveltekit/ToolbarButton.svelte.d.ts +12 -0
- package/dist/sveltekit/context.d.ts +16 -0
- package/dist/sveltekit/context.js +9 -0
- package/dist/sveltekit/index.d.ts +6 -0
- package/dist/sveltekit/index.js +6 -0
- package/dist/ui/Account.svelte +106 -0
- package/dist/ui/Account.svelte.d.ts +8 -0
- package/dist/ui/App.svelte +77 -0
- package/dist/ui/App.svelte.d.ts +7 -0
- package/dist/ui/Avatar.svelte +64 -0
- package/dist/ui/Avatar.svelte.d.ts +11 -0
- package/dist/ui/Composer.svelte +267 -0
- package/dist/ui/Composer.svelte.d.ts +20 -0
- package/dist/ui/Connect.svelte +128 -0
- package/dist/ui/Connect.svelte.d.ts +6 -0
- package/dist/ui/Icon.svelte +44 -0
- package/dist/ui/Icon.svelte.d.ts +29 -0
- package/dist/ui/Menu.svelte +69 -0
- package/dist/ui/Menu.svelte.d.ts +15 -0
- package/dist/ui/MenuItem.svelte +38 -0
- package/dist/ui/MenuItem.svelte.d.ts +12 -0
- package/dist/ui/Overlay.svelte +502 -0
- package/dist/ui/Overlay.svelte.d.ts +3 -0
- package/dist/ui/Panel.svelte +220 -0
- package/dist/ui/Panel.svelte.d.ts +10 -0
- package/dist/ui/Pin.svelte +156 -0
- package/dist/ui/Pin.svelte.d.ts +15 -0
- package/dist/ui/Presence.svelte +43 -0
- package/dist/ui/Presence.svelte.d.ts +8 -0
- package/dist/ui/ThreadCard.svelte +201 -0
- package/dist/ui/ThreadCard.svelte.d.ts +13 -0
- package/dist/ui/Toolbar.svelte +35 -0
- package/dist/ui/Toolbar.svelte.d.ts +18 -0
- package/dist/ui/context.d.ts +3 -0
- package/dist/ui/context.js +8 -0
- package/dist/ui/generated/styles.css +2 -0
- package/dist/ui/generated/styles.d.ts +2 -0
- package/dist/ui/generated/styles.js +3 -0
- package/dist/ui/styles.css +113 -0
- package/dist/ui/text.d.ts +5 -0
- package/dist/ui/text.js +11 -0
- package/dist/visual-comments.svelte.d.ts +230 -0
- package/dist/visual-comments.svelte.js +693 -0
- package/dist/vite/index.d.ts +20 -0
- package/dist/vite/index.js +78 -0
- package/dist-widget/visual-comments.js +58 -0
- package/package.json +86 -3
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
<svelte:options css="injected" />
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Pins, the new-comment field and the open thread, drawn over the page in
|
|
5
|
+
client pixels. Each surface says where its pixels are on screen, so pins
|
|
6
|
+
follow it however it is zoomed, panned or scrolled. Cards open on whichever
|
|
7
|
+
side of a pin has room inside the surface's stage. It also outlines the
|
|
8
|
+
element the pointer is aiming at, the new comment's element and the open
|
|
9
|
+
thread's.
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { untrack } from 'svelte';
|
|
13
|
+
import { useComments } from './context.js';
|
|
14
|
+
import Composer from './Composer.svelte';
|
|
15
|
+
import Pin from './Pin.svelte';
|
|
16
|
+
import ThreadCard from './ThreadCard.svelte';
|
|
17
|
+
import Icon from './Icon.svelte';
|
|
18
|
+
import { describeTarget, MAGNET, widen, type Box } from '../core/target.js';
|
|
19
|
+
import { docBoxToClient, docBoxToSurface, intersect, spotOf, toClient, type Rect, type Surface } from '../core/surfaces.js';
|
|
20
|
+
import { neck } from '../core/metaball.js';
|
|
21
|
+
import { activity } from '../core/presence.js';
|
|
22
|
+
import type { CommentImage, CommentThread } from '../shared/types.js';
|
|
23
|
+
|
|
24
|
+
const vc = useComments();
|
|
25
|
+
const comments = vc.store;
|
|
26
|
+
const PIN = 32;
|
|
27
|
+
const GAP = 10;
|
|
28
|
+
const EDGE = 8;
|
|
29
|
+
const CARD_WIDTH = 320;
|
|
30
|
+
const COMPOSER_WIDTH = 296;
|
|
31
|
+
|
|
32
|
+
let composer = $state<Composer>();
|
|
33
|
+
let cardHeight = $state(0);
|
|
34
|
+
let shake = $state(false);
|
|
35
|
+
|
|
36
|
+
/** Brings the unsent comment back to attention instead of discarding it. */
|
|
37
|
+
function focusDraft() {
|
|
38
|
+
composer?.focus();
|
|
39
|
+
shake = false;
|
|
40
|
+
requestAnimationFrame(() => (shake = true));
|
|
41
|
+
}
|
|
42
|
+
$effect(() => {
|
|
43
|
+
vc.onFocusDraft(focusDraft);
|
|
44
|
+
return () => vc.onFocusDraft(undefined);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Where cards on a surface can go: its stage, clear of the list. */
|
|
48
|
+
function stageOf(surface: Surface): Rect {
|
|
49
|
+
const panel = vc.active && !vc.panelCollapsed ? vc.panelRect ?? floatingPanel() : undefined;
|
|
50
|
+
const stage = intersect(surface.stage, { left: 0, top: 0, right: innerWidth, bottom: innerHeight });
|
|
51
|
+
return panel && panel.left < stage.right && panel.left > stage.left ? { ...stage, right: panel.left } : stage;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The list's place when the host gives it none: along the right edge. */
|
|
55
|
+
function floatingPanel(): Rect {
|
|
56
|
+
return { left: innerWidth - 12 - 320, top: 12, right: innerWidth - 12, bottom: innerHeight - 12 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A thread's pin where it is now, or undefined while it is not on screen. */
|
|
60
|
+
function spot(thread: CommentThread) {
|
|
61
|
+
void vc.layout;
|
|
62
|
+
const surface = vc.surface(thread.surface);
|
|
63
|
+
if (!surface) return undefined;
|
|
64
|
+
return { surface, ...spotOf(surface, thread) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Opening a thread scrolls its element into view inside its document, once found.
|
|
68
|
+
$effect(() => {
|
|
69
|
+
void vc.layout;
|
|
70
|
+
const id = vc.revealing;
|
|
71
|
+
if (id === undefined) return;
|
|
72
|
+
untrack(() => {
|
|
73
|
+
const thread = comments.threads.find((t) => t.id === id);
|
|
74
|
+
if (!thread?.anchor) return void (vc.revealing = undefined);
|
|
75
|
+
const where = spot(thread);
|
|
76
|
+
if (!where?.surface.doc) return;
|
|
77
|
+
if (where.element && where.hidden) vc.revealElement(where.element);
|
|
78
|
+
else vc.revealing = undefined;
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const pins = $derived(vc.pinned.flatMap((thread) => {
|
|
83
|
+
const where = spot(thread);
|
|
84
|
+
return where && !where.hidden ? [{ thread, left: where.x, top: where.y }] : [];
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
/** Per thread, what those working on it say they are doing. */
|
|
88
|
+
const working = $derived.by(() => {
|
|
89
|
+
const lines = new Map<number, string>();
|
|
90
|
+
for (const participant of vc.participants) {
|
|
91
|
+
if (participant.state !== 'working' || participant.thread === undefined) continue;
|
|
92
|
+
const line = activity(participant)!;
|
|
93
|
+
const before = lines.get(participant.thread);
|
|
94
|
+
lines.set(participant.thread, before ? `${before} · ${line}` : line);
|
|
95
|
+
}
|
|
96
|
+
return lines;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/** Beside the pin, on the side with room, kept inside the stage. */
|
|
100
|
+
function beside(at: { left: number; top: number }, width: number, height: number, bounds: Rect) {
|
|
101
|
+
let left = at.left + PIN + GAP;
|
|
102
|
+
if (left + width > bounds.right - EDGE) left = at.left - GAP - width;
|
|
103
|
+
left = Math.max(bounds.left + EDGE, Math.min(left, bounds.right - EDGE - width));
|
|
104
|
+
const top = Math.max(bounds.top + EDGE, Math.min(at.top - PIN, bounds.bottom - EDGE - height));
|
|
105
|
+
return { left, top };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const open = $derived.by(() => {
|
|
109
|
+
const thread = vc.open;
|
|
110
|
+
const where = thread && spot(thread);
|
|
111
|
+
if (!thread || !where || where.hidden) return undefined;
|
|
112
|
+
const bounds = stageOf(where.surface);
|
|
113
|
+
const at = { left: where.x, top: where.y };
|
|
114
|
+
return { thread, where, card: beside(at, CARD_WIDTH, cardHeight, bounds), maxHeight: bounds.bottom - bounds.top - EDGE * 2 };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** A client box with a little room around it, and its corner radius on screen. */
|
|
118
|
+
function outline(rect: Rect, radius = 0) {
|
|
119
|
+
const pad = 3;
|
|
120
|
+
return {
|
|
121
|
+
left: rect.left - pad,
|
|
122
|
+
top: rect.top - pad,
|
|
123
|
+
width: rect.right - rect.left + pad * 2,
|
|
124
|
+
height: rect.bottom - rect.top + pad * 2,
|
|
125
|
+
radius: radius + pad
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const aimSurface = $derived(vc.aim && vc.surface(vc.aim.surface));
|
|
130
|
+
const aiming = $derived.by(() => {
|
|
131
|
+
const aim = vc.aim;
|
|
132
|
+
if (!aim?.target || !aimSurface) return undefined;
|
|
133
|
+
const shape = outline(docBoxToClient(aimSurface, aim.target.box), aim.target.radius * aimSurface.docScale);
|
|
134
|
+
// Like a pointer resting on a button, the outline leans a little toward the pointer.
|
|
135
|
+
const box = aim.targetBox!;
|
|
136
|
+
const lean = (offset: number, size: number) => Math.max(-1, Math.min(1, offset / (size / 2 || 1))) * 3;
|
|
137
|
+
return {
|
|
138
|
+
...shape,
|
|
139
|
+
dx: lean(aim.point.x - (box.x + box.width / 2), box.width),
|
|
140
|
+
dy: lean(aim.point.y - (box.y + box.height / 2), box.height)
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Over an element the cursor becomes a faint dot, joined to the outline
|
|
146
|
+
* while the pointer is outside it.
|
|
147
|
+
*/
|
|
148
|
+
const DOT = 4;
|
|
149
|
+
const dot = $derived.by(() => {
|
|
150
|
+
const cursor = vc.cursor;
|
|
151
|
+
if (!cursor || !aiming) return undefined;
|
|
152
|
+
const box = {
|
|
153
|
+
left: aiming.left + aiming.dx,
|
|
154
|
+
top: aiming.top + aiming.dy,
|
|
155
|
+
width: aiming.width,
|
|
156
|
+
height: aiming.height,
|
|
157
|
+
radius: aiming.radius
|
|
158
|
+
};
|
|
159
|
+
return { ...cursor, neck: neck(cursor.x, cursor.y, DOT, box, MAGNET + 8) };
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const draftSurface = $derived(vc.draft && vc.surface(vc.draft.surface));
|
|
163
|
+
/** The new comment's element where it is now, following any scroll since the click, in surface pixels. */
|
|
164
|
+
const draftBox = $derived.by((): Box | undefined => {
|
|
165
|
+
void vc.layout;
|
|
166
|
+
const target = vc.draft?.target;
|
|
167
|
+
if (!target || !draftSurface) return undefined;
|
|
168
|
+
if (!target.element.isConnected) return vc.draft!.targetBox;
|
|
169
|
+
const r = target.element.getBoundingClientRect();
|
|
170
|
+
return docBoxToSurface(draftSurface, { x: r.left, y: r.top, width: r.width, height: r.height });
|
|
171
|
+
});
|
|
172
|
+
const drafted = $derived.by(() => {
|
|
173
|
+
const target = vc.draft?.target;
|
|
174
|
+
if (!target || !draftBox || !draftSurface) return undefined;
|
|
175
|
+
const a = toClient(draftSurface, draftBox.x, draftBox.y);
|
|
176
|
+
const s = draftSurface.scale;
|
|
177
|
+
return outline({ left: a.x, top: a.y, right: a.x + draftBox.width * s, bottom: a.y + draftBox.height * s }, target.radius * draftSurface.docScale);
|
|
178
|
+
});
|
|
179
|
+
const wider = $derived(vc.draft?.target && draftSurface && widen(vc.draft.target, draftSurface.search));
|
|
180
|
+
const anchored = $derived(open?.thread.anchor && open.where.box && outline(open.where.box));
|
|
181
|
+
/** The new comment's element, or else the open thread's. */
|
|
182
|
+
const marked = $derived(drafted || (vc.draft ? undefined : anchored));
|
|
183
|
+
|
|
184
|
+
/** Moves the comment to another element, keeping the pin at the same place on screen. */
|
|
185
|
+
function retarget(target: NonNullable<typeof vc.draft>['target']) {
|
|
186
|
+
const draft = vc.draft;
|
|
187
|
+
if (!draft || !draftSurface || !target) return;
|
|
188
|
+
draft.target = target;
|
|
189
|
+
draft.targetBox = docBoxToSurface(draftSurface, target.box);
|
|
190
|
+
composer?.focus();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function widenDraft() {
|
|
194
|
+
if (!vc.draft?.target || !wider) return;
|
|
195
|
+
vc.draft.inner = [vc.draft.target, ...(vc.draft.inner ?? [])];
|
|
196
|
+
retarget(wider);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function narrowDraft() {
|
|
200
|
+
const [inner, ...rest] = vc.draft?.inner ?? [];
|
|
201
|
+
if (!vc.draft || !inner) return;
|
|
202
|
+
vc.draft.inner = rest;
|
|
203
|
+
retarget(inner);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Leaves the element and pins the comment where the pointer was. */
|
|
207
|
+
function detachDraft() {
|
|
208
|
+
const draft = vc.draft;
|
|
209
|
+
if (!draft) return;
|
|
210
|
+
draft.x = draft.point.x;
|
|
211
|
+
draft.y = draft.point.y;
|
|
212
|
+
draft.target = undefined;
|
|
213
|
+
draft.targetBox = undefined;
|
|
214
|
+
draft.inner = undefined;
|
|
215
|
+
composer?.focus();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function onDraftKeydown(event: KeyboardEvent) {
|
|
219
|
+
if (!event.altKey || event.metaKey || event.ctrlKey) return;
|
|
220
|
+
if (event.key === 'ArrowUp') widenDraft();
|
|
221
|
+
else if (event.key === 'ArrowDown') narrowDraft();
|
|
222
|
+
else return;
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const pending = $derived.by(() => {
|
|
227
|
+
const draft = vc.draft;
|
|
228
|
+
if (!draft || !draftSurface) return undefined;
|
|
229
|
+
const box = draft.targetBox && draftBox;
|
|
230
|
+
const x = box ? box.x + draft.x - draft.targetBox!.x : draft.x;
|
|
231
|
+
const y = box ? box.y + draft.y - draft.targetBox!.y : draft.y;
|
|
232
|
+
const c = toClient(draftSurface, x, y);
|
|
233
|
+
const at = { left: c.x, top: c.y };
|
|
234
|
+
return { at, card: beside({ left: at.left, top: at.top + 4 }, COMPOSER_WIDTH, 40, stageOf(draftSurface)) };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
async function submitDraft(body: string, images: CommentImage[]) {
|
|
238
|
+
const draft = vc.draft;
|
|
239
|
+
const surface = draftSurface;
|
|
240
|
+
if (!draft || !surface) return;
|
|
241
|
+
try {
|
|
242
|
+
const anchor = draft.target && draft.targetBox
|
|
243
|
+
? { ...describeTarget(draft.target), ...draft.targetBox }
|
|
244
|
+
: null;
|
|
245
|
+
const thread = await comments.create({
|
|
246
|
+
page: vc.page,
|
|
247
|
+
url: location.href,
|
|
248
|
+
surface: surface.id,
|
|
249
|
+
title: surface.title || document.title,
|
|
250
|
+
x: draft.x,
|
|
251
|
+
y: draft.y,
|
|
252
|
+
anchor,
|
|
253
|
+
context: contextFor(surface),
|
|
254
|
+
body
|
|
255
|
+
}, images);
|
|
256
|
+
vc.draft = undefined;
|
|
257
|
+
vc.openThread(thread.id);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
comments.error = (error as Error).message;
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function contextFor(surface: Surface): Record<string, string> {
|
|
265
|
+
const extra = vc.options.context?.({ id: surface.id, title: surface.title, element: surface.element }) ?? {};
|
|
266
|
+
const entries = Object.entries({ viewport: `${innerWidth}×${innerHeight}`, ...surface.context, ...extra })
|
|
267
|
+
.filter(([, value]) => value !== undefined && value !== '')
|
|
268
|
+
.map(([key, value]) => [key, String(value)]);
|
|
269
|
+
return Object.fromEntries(entries);
|
|
270
|
+
}
|
|
271
|
+
</script>
|
|
272
|
+
|
|
273
|
+
{#if vc.cursor}
|
|
274
|
+
<!-- The .vc-comment-cursor image, drawn. Its hotspot is the bottom-left point. -->
|
|
275
|
+
<svg
|
|
276
|
+
class={['vc-cursor', aiming && 'vc-cursor-hidden']}
|
|
277
|
+
style:left="{vc.cursor.x - 1}px"
|
|
278
|
+
style:top="{vc.cursor.y - 27}px"
|
|
279
|
+
width="28"
|
|
280
|
+
height="28"
|
|
281
|
+
viewBox="0 0 28 28"
|
|
282
|
+
aria-hidden="true"
|
|
283
|
+
>
|
|
284
|
+
<path d="M14 1.5a12.5 12.5 0 1 1 0 25H1.5V14A12.5 12.5 0 0 1 14 1.5z" fill="white" stroke="#1c1917" stroke-width="1.5" />
|
|
285
|
+
<path d="M14 9v10M9 14h10" stroke="#1c1917" stroke-width="1.5" stroke-linecap="round" />
|
|
286
|
+
</svg>
|
|
287
|
+
{/if}
|
|
288
|
+
{#if dot}
|
|
289
|
+
<svg class="vc-dot" aria-hidden="true">
|
|
290
|
+
<g>
|
|
291
|
+
{#if dot.neck}<path d={dot.neck} />{/if}
|
|
292
|
+
<circle cx={dot.x} cy={dot.y} r={DOT} />
|
|
293
|
+
</g>
|
|
294
|
+
</svg>
|
|
295
|
+
{/if}
|
|
296
|
+
{#if aiming}
|
|
297
|
+
<span
|
|
298
|
+
class="vc-target vc-target-aim absolute"
|
|
299
|
+
style:left="{aiming.left}px"
|
|
300
|
+
style:top="{aiming.top}px"
|
|
301
|
+
style:width="{aiming.width}px"
|
|
302
|
+
style:height="{aiming.height}px"
|
|
303
|
+
style:border-radius="{aiming.radius}px"
|
|
304
|
+
style:translate="{aiming.dx}px {aiming.dy}px"
|
|
305
|
+
aria-hidden="true"
|
|
306
|
+
></span>
|
|
307
|
+
{/if}
|
|
308
|
+
{#if marked}
|
|
309
|
+
<span
|
|
310
|
+
class={['vc-target absolute', !drafted && 'vc-target-quiet']}
|
|
311
|
+
style:left="{marked.left}px"
|
|
312
|
+
style:top="{marked.top}px"
|
|
313
|
+
style:width="{marked.width}px"
|
|
314
|
+
style:height="{marked.height}px"
|
|
315
|
+
style:border-radius="{marked.radius}px"
|
|
316
|
+
aria-hidden="true"
|
|
317
|
+
></span>
|
|
318
|
+
{/if}
|
|
319
|
+
|
|
320
|
+
{#each pins as pin (pin.thread.id)}
|
|
321
|
+
<Pin
|
|
322
|
+
thread={pin.thread}
|
|
323
|
+
left={pin.left}
|
|
324
|
+
top={pin.top}
|
|
325
|
+
now={comments.now}
|
|
326
|
+
working={working.get(pin.thread.id)}
|
|
327
|
+
open={pin.thread.id === vc.openId}
|
|
328
|
+
onopen={() => {
|
|
329
|
+
vc.draft = undefined;
|
|
330
|
+
vc.openThread(pin.thread.id === vc.openId ? undefined : pin.thread.id);
|
|
331
|
+
}}
|
|
332
|
+
/>
|
|
333
|
+
{/each}
|
|
334
|
+
|
|
335
|
+
{#if pending && vc.draft}
|
|
336
|
+
<span
|
|
337
|
+
class="vc-draft-pin absolute z-20 block size-8 bg-vc-comment shadow-[0_1px_4px_rgb(0_0_0/0.3)]"
|
|
338
|
+
style:left="{pending.at.left}px"
|
|
339
|
+
style:top="{pending.at.top}px"
|
|
340
|
+
aria-hidden="true"
|
|
341
|
+
></span>
|
|
342
|
+
<!-- Alt+Up and Alt+Down from the field widen or narrow the comment's element. -->
|
|
343
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
344
|
+
<div
|
|
345
|
+
data-vc-ui
|
|
346
|
+
class={['pointer-events-auto absolute z-40 cursor-default', shake && 'vc-shake']}
|
|
347
|
+
style:left="{pending.card.left}px"
|
|
348
|
+
style:top="{pending.card.top}px"
|
|
349
|
+
role="group"
|
|
350
|
+
aria-label="New comment"
|
|
351
|
+
onanimationend={() => (shake = false)}
|
|
352
|
+
onkeydown={onDraftKeydown}
|
|
353
|
+
>
|
|
354
|
+
{#if vc.viewer}
|
|
355
|
+
<Composer
|
|
356
|
+
bind:this={composer}
|
|
357
|
+
variant="new"
|
|
358
|
+
placeholder="Add a comment"
|
|
359
|
+
initial={vc.draft.text}
|
|
360
|
+
autofocus
|
|
361
|
+
oninput={(text) => { if (vc.draft) vc.draft.text = text; }}
|
|
362
|
+
onimages={(count) => { if (vc.draft) vc.draft.images = count; }}
|
|
363
|
+
onsubmit={submitDraft}
|
|
364
|
+
oncancel={() => (vc.draft = undefined)}
|
|
365
|
+
/>
|
|
366
|
+
{#if vc.draft.target}
|
|
367
|
+
<div class="mt-1.5 flex w-fit max-w-[296px] items-center rounded-full bg-white py-0.5 pr-0.5 pl-3 text-xs text-vc-muted shadow-[0_1px_6px_rgb(0_0_0/0.16)]">
|
|
368
|
+
<span class="min-w-0 truncate pr-1" title={vc.draft.target.label}>On {vc.draft.target.label}</span>
|
|
369
|
+
{#if vc.draft.inner?.length}
|
|
370
|
+
<button type="button" class="vc-target-tool" aria-label="Select the element inside" title="Inside (⌥↓)" onclick={narrowDraft}>
|
|
371
|
+
<Icon name="chevron-down" size={14} />
|
|
372
|
+
</button>
|
|
373
|
+
{/if}
|
|
374
|
+
{#if wider}
|
|
375
|
+
<button type="button" class="vc-target-tool" aria-label="Select the element around it" title="Around it (⌥↑)" onclick={widenDraft}>
|
|
376
|
+
<Icon name="chevron-up" size={14} />
|
|
377
|
+
</button>
|
|
378
|
+
{/if}
|
|
379
|
+
<button type="button" class="vc-target-tool" aria-label="Pin to the point instead" title="Pin to the point instead" onclick={detachDraft}>
|
|
380
|
+
<Icon name="close" size={14} />
|
|
381
|
+
</button>
|
|
382
|
+
</div>
|
|
383
|
+
{/if}
|
|
384
|
+
{:else}
|
|
385
|
+
<div class="flex w-[296px] items-center gap-3 rounded-[18px] bg-white py-2 pr-2 pl-4 text-[13px] text-vc-ink shadow-[0_2px_14px_rgb(0_0_0/0.18)]">
|
|
386
|
+
<p class="flex-1">Sign in to comment.</p>
|
|
387
|
+
<button type="button" class="h-7 rounded-full bg-vc-comment px-3 font-medium text-white hover:brightness-110"
|
|
388
|
+
onclick={() => vc.signIn()}>Sign in</button>
|
|
389
|
+
</div>
|
|
390
|
+
{/if}
|
|
391
|
+
</div>
|
|
392
|
+
{/if}
|
|
393
|
+
|
|
394
|
+
{#if open}
|
|
395
|
+
<div
|
|
396
|
+
class="absolute z-40"
|
|
397
|
+
style:left="{open.card.left}px"
|
|
398
|
+
style:top="{open.card.top}px"
|
|
399
|
+
bind:offsetHeight={cardHeight}
|
|
400
|
+
>
|
|
401
|
+
<ThreadCard
|
|
402
|
+
thread={open.thread}
|
|
403
|
+
title={vc.titleOf(open.thread)}
|
|
404
|
+
link={vc.linkOf(open.thread)}
|
|
405
|
+
maxHeight={Math.max(200, open.maxHeight)}
|
|
406
|
+
onclose={() => vc.openThread(undefined)}
|
|
407
|
+
/>
|
|
408
|
+
</div>
|
|
409
|
+
{/if}
|
|
410
|
+
|
|
411
|
+
<style>
|
|
412
|
+
.vc-cursor {
|
|
413
|
+
position: fixed;
|
|
414
|
+
z-index: 50;
|
|
415
|
+
transform-origin: 1px 27px;
|
|
416
|
+
transition: opacity 140ms ease, scale 140ms ease;
|
|
417
|
+
}
|
|
418
|
+
.vc-cursor-hidden {
|
|
419
|
+
opacity: 0;
|
|
420
|
+
scale: 0.6;
|
|
421
|
+
}
|
|
422
|
+
.vc-dot {
|
|
423
|
+
position: fixed;
|
|
424
|
+
inset: 0;
|
|
425
|
+
z-index: 50;
|
|
426
|
+
width: 100%;
|
|
427
|
+
height: 100%;
|
|
428
|
+
overflow: visible;
|
|
429
|
+
animation: vc-dot-in 140ms ease;
|
|
430
|
+
}
|
|
431
|
+
/* One group, so where the neck overlaps the dot it is not darker. */
|
|
432
|
+
.vc-dot g {
|
|
433
|
+
fill: var(--color-vc-comment);
|
|
434
|
+
opacity: 0.5;
|
|
435
|
+
}
|
|
436
|
+
@keyframes vc-dot-in {
|
|
437
|
+
from {
|
|
438
|
+
opacity: 0;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
.vc-target {
|
|
442
|
+
border: 2px solid var(--color-vc-comment);
|
|
443
|
+
background: color-mix(in srgb, var(--color-vc-comment) 8%, transparent);
|
|
444
|
+
}
|
|
445
|
+
.vc-target-aim {
|
|
446
|
+
transition-property: left, top, width, height, border-radius, translate;
|
|
447
|
+
transition-duration: 110ms, 110ms, 110ms, 110ms, 110ms, 60ms;
|
|
448
|
+
transition-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
449
|
+
animation: vc-target-in 110ms ease-out;
|
|
450
|
+
}
|
|
451
|
+
@keyframes vc-target-in {
|
|
452
|
+
from {
|
|
453
|
+
opacity: 0;
|
|
454
|
+
scale: 1.06;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
.vc-target-quiet {
|
|
458
|
+
border: 1.5px dashed var(--color-vc-comment);
|
|
459
|
+
background: none;
|
|
460
|
+
}
|
|
461
|
+
.vc-target-tool {
|
|
462
|
+
display: grid;
|
|
463
|
+
place-items: center;
|
|
464
|
+
width: 22px;
|
|
465
|
+
height: 22px;
|
|
466
|
+
border-radius: 999px;
|
|
467
|
+
color: var(--color-vc-muted);
|
|
468
|
+
}
|
|
469
|
+
.vc-target-tool:hover {
|
|
470
|
+
background: var(--color-vc-wash);
|
|
471
|
+
color: var(--color-vc-ink);
|
|
472
|
+
}
|
|
473
|
+
.vc-draft-pin {
|
|
474
|
+
border-radius: 16px 16px 16px 0;
|
|
475
|
+
transform: translateY(-100%);
|
|
476
|
+
animation: vc-drop 180ms cubic-bezier(0.2, 0.9, 0.3, 1.3);
|
|
477
|
+
transform-origin: bottom left;
|
|
478
|
+
}
|
|
479
|
+
@keyframes vc-drop {
|
|
480
|
+
from {
|
|
481
|
+
transform: translateY(-100%) scale(0.4);
|
|
482
|
+
opacity: 0;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
.vc-shake {
|
|
486
|
+
animation: vc-shake 320ms ease;
|
|
487
|
+
}
|
|
488
|
+
@keyframes vc-shake {
|
|
489
|
+
20%, 60% { translate: -6px 0; }
|
|
490
|
+
40%, 80% { translate: 6px 0; }
|
|
491
|
+
}
|
|
492
|
+
@media (prefers-reduced-motion: reduce) {
|
|
493
|
+
.vc-draft-pin,
|
|
494
|
+
.vc-shake,
|
|
495
|
+
.vc-target-aim,
|
|
496
|
+
.vc-cursor,
|
|
497
|
+
.vc-dot {
|
|
498
|
+
animation: none;
|
|
499
|
+
transition: none;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
</style>
|