skema-core 0.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/README.md +116 -0
- package/dist/cli.js +37 -0
- package/dist/index.d.mts +356 -0
- package/dist/index.d.ts +356 -0
- package/dist/index.js +2334 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2307 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +260 -0
- package/dist/server.d.ts +260 -0
- package/dist/server.js +476 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +465 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +76 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2307 @@
|
|
|
1
|
+
import { forwardRef, useState, useRef, useEffect, useCallback, useImperativeHandle, useMemo } from 'react';
|
|
2
|
+
import { StateNode, atom, Box, ArrowShapeKindStyle, Tldraw, TldrawOverlays, useEditor, useTools, useIsToolSelected, useValue } from 'tldraw';
|
|
3
|
+
import 'tldraw/tldraw.css';
|
|
4
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/components/Skema.tsx
|
|
7
|
+
var IdleState = class extends StateNode {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.onPointerDown = (info) => {
|
|
11
|
+
this.parent.transition("lassoing", info);
|
|
12
|
+
};
|
|
13
|
+
this.onCancel = () => {
|
|
14
|
+
this.editor.setCurrentTool("select");
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
IdleState.id = "idle";
|
|
19
|
+
var LassoingState = class extends StateNode {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
// Reactive atom to store lasso points for rendering
|
|
23
|
+
this.points = atom("lasso points", []);
|
|
24
|
+
// Track starting point to detect clicks vs drags
|
|
25
|
+
this.startPoint = null;
|
|
26
|
+
this.onEnter = (info) => {
|
|
27
|
+
const { currentPagePoint } = this.editor.inputs;
|
|
28
|
+
this.startPoint = { x: currentPagePoint.x, y: currentPagePoint.y };
|
|
29
|
+
this.points.set([{ x: currentPagePoint.x, y: currentPagePoint.y }]);
|
|
30
|
+
};
|
|
31
|
+
this.onPointerMove = () => {
|
|
32
|
+
const { currentPagePoint } = this.editor.inputs;
|
|
33
|
+
const currentPoints = this.points.get();
|
|
34
|
+
const lastPoint = currentPoints[currentPoints.length - 1];
|
|
35
|
+
if (lastPoint) {
|
|
36
|
+
const dx = currentPagePoint.x - lastPoint.x;
|
|
37
|
+
const dy = currentPagePoint.y - lastPoint.y;
|
|
38
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
39
|
+
if (dist > 3) {
|
|
40
|
+
this.points.set([...currentPoints, { x: currentPagePoint.x, y: currentPagePoint.y }]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
this.onPointerUp = () => {
|
|
45
|
+
this.complete();
|
|
46
|
+
};
|
|
47
|
+
this.onCancel = () => {
|
|
48
|
+
this.cancel();
|
|
49
|
+
};
|
|
50
|
+
this.onComplete = () => {
|
|
51
|
+
this.complete();
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
complete() {
|
|
55
|
+
const points = this.points.get();
|
|
56
|
+
const { currentPagePoint } = this.editor.inputs;
|
|
57
|
+
if (this.startPoint) {
|
|
58
|
+
const dx = currentPagePoint.x - this.startPoint.x;
|
|
59
|
+
const dy = currentPagePoint.y - this.startPoint.y;
|
|
60
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
61
|
+
if (dist < 10 || points.length < 3) {
|
|
62
|
+
this.editor.setSelectedShapes([]);
|
|
63
|
+
const tool = this.parent;
|
|
64
|
+
tool.onClearSelections?.();
|
|
65
|
+
this.points.set([]);
|
|
66
|
+
this.startPoint = null;
|
|
67
|
+
this.parent.transition("idle");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (points.length > 2) {
|
|
72
|
+
this.selectShapesInLasso(points);
|
|
73
|
+
const tool = this.parent;
|
|
74
|
+
tool.onLassoComplete?.(points);
|
|
75
|
+
}
|
|
76
|
+
this.points.set([]);
|
|
77
|
+
this.startPoint = null;
|
|
78
|
+
this.parent.transition("idle");
|
|
79
|
+
}
|
|
80
|
+
cancel() {
|
|
81
|
+
this.points.set([]);
|
|
82
|
+
this.startPoint = null;
|
|
83
|
+
this.parent.transition("idle");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Select shapes that intersect with the lasso area (any overlap counts)
|
|
87
|
+
*/
|
|
88
|
+
selectShapesInLasso(points) {
|
|
89
|
+
const allShapes = this.editor.getCurrentPageShapes();
|
|
90
|
+
if (allShapes.length === 0) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let minX = Infinity, minY = Infinity;
|
|
94
|
+
let maxX = -Infinity, maxY = -Infinity;
|
|
95
|
+
for (const p of points) {
|
|
96
|
+
minX = Math.min(minX, p.x);
|
|
97
|
+
minY = Math.min(minY, p.y);
|
|
98
|
+
maxX = Math.max(maxX, p.x);
|
|
99
|
+
maxY = Math.max(maxY, p.y);
|
|
100
|
+
}
|
|
101
|
+
const selectedIds = [];
|
|
102
|
+
for (const shape of allShapes) {
|
|
103
|
+
const shapeBounds = this.editor.getShapePageBounds(shape.id);
|
|
104
|
+
if (!shapeBounds) continue;
|
|
105
|
+
const boundsOverlap = !(shapeBounds.x > maxX || shapeBounds.x + shapeBounds.width < minX || shapeBounds.y > maxY || shapeBounds.y + shapeBounds.height < minY);
|
|
106
|
+
if (boundsOverlap) {
|
|
107
|
+
selectedIds.push(shape.id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (selectedIds.length > 0) {
|
|
111
|
+
this.editor.setSelectedShapes(selectedIds);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ray casting algorithm to check if a point is inside a polygon
|
|
116
|
+
*/
|
|
117
|
+
isPointInPolygon(point, polygon) {
|
|
118
|
+
let inside = false;
|
|
119
|
+
const n = polygon.length;
|
|
120
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
121
|
+
const xi = polygon[i].x;
|
|
122
|
+
const yi = polygon[i].y;
|
|
123
|
+
const xj = polygon[j].x;
|
|
124
|
+
const yj = polygon[j].y;
|
|
125
|
+
const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
|
|
126
|
+
if (intersect) {
|
|
127
|
+
inside = !inside;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return inside;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get the bounding box of the lasso in page coordinates
|
|
134
|
+
*/
|
|
135
|
+
getLassoBounds() {
|
|
136
|
+
const points = this.points.get();
|
|
137
|
+
if (points.length < 2) return null;
|
|
138
|
+
let minX = Infinity;
|
|
139
|
+
let minY = Infinity;
|
|
140
|
+
let maxX = -Infinity;
|
|
141
|
+
let maxY = -Infinity;
|
|
142
|
+
for (const point of points) {
|
|
143
|
+
minX = Math.min(minX, point.x);
|
|
144
|
+
minY = Math.min(minY, point.y);
|
|
145
|
+
maxX = Math.max(maxX, point.x);
|
|
146
|
+
maxY = Math.max(maxY, point.y);
|
|
147
|
+
}
|
|
148
|
+
return new Box(minX, minY, maxX - minX, maxY - minY);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
LassoingState.id = "lassoing";
|
|
152
|
+
var LassoSelectTool = class extends StateNode {
|
|
153
|
+
constructor() {
|
|
154
|
+
super(...arguments);
|
|
155
|
+
// Callback to clear DOM selections (set by Skema component)
|
|
156
|
+
this.onClearSelections = null;
|
|
157
|
+
// Callback when lasso selection completes (set by Skema component)
|
|
158
|
+
this.onLassoComplete = null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Set callback for clearing DOM selections
|
|
162
|
+
*/
|
|
163
|
+
setOnClearSelections(callback) {
|
|
164
|
+
this.onClearSelections = callback;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Set callback for when lasso selection completes
|
|
168
|
+
*/
|
|
169
|
+
setOnLassoComplete(callback) {
|
|
170
|
+
this.onLassoComplete = callback;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
LassoSelectTool.id = "lasso-select";
|
|
174
|
+
LassoSelectTool.initial = "idle";
|
|
175
|
+
LassoSelectTool.children = () => [IdleState, LassoingState];
|
|
176
|
+
|
|
177
|
+
// src/utils/coordinates.ts
|
|
178
|
+
function getViewportInfo() {
|
|
179
|
+
if (typeof window === "undefined") {
|
|
180
|
+
return { width: 0, height: 0, scrollX: 0, scrollY: 0 };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
width: window.innerWidth,
|
|
184
|
+
height: window.innerHeight,
|
|
185
|
+
scrollX: window.scrollX,
|
|
186
|
+
scrollY: window.scrollY
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function viewportToDocument(x, y, viewport = getViewportInfo()) {
|
|
190
|
+
return {
|
|
191
|
+
x: x + viewport.scrollX,
|
|
192
|
+
y: y + viewport.scrollY
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function documentToViewport(x, y, viewport = getViewportInfo()) {
|
|
196
|
+
return {
|
|
197
|
+
x: x - viewport.scrollX,
|
|
198
|
+
y: y - viewport.scrollY
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function bboxViewportToDocument(bbox, viewport = getViewportInfo()) {
|
|
202
|
+
const { x, y } = viewportToDocument(bbox.x, bbox.y, viewport);
|
|
203
|
+
return {
|
|
204
|
+
x,
|
|
205
|
+
y,
|
|
206
|
+
width: bbox.width,
|
|
207
|
+
height: bbox.height
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function bboxDocumentToViewport(bbox, viewport = getViewportInfo()) {
|
|
211
|
+
const { x, y } = documentToViewport(bbox.x, bbox.y, viewport);
|
|
212
|
+
return {
|
|
213
|
+
x,
|
|
214
|
+
y,
|
|
215
|
+
width: bbox.width,
|
|
216
|
+
height: bbox.height
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function bboxIntersects(a, b) {
|
|
220
|
+
return !(a.x + a.width < b.x || b.x + b.width < a.x || a.y + a.height < b.y || b.y + b.height < a.y);
|
|
221
|
+
}
|
|
222
|
+
function pointInBbox(x, y, bbox) {
|
|
223
|
+
return x >= bbox.x && x <= bbox.x + bbox.width && y >= bbox.y && y <= bbox.y + bbox.height;
|
|
224
|
+
}
|
|
225
|
+
function bboxCenter(bbox) {
|
|
226
|
+
return {
|
|
227
|
+
x: bbox.x + bbox.width / 2,
|
|
228
|
+
y: bbox.y + bbox.height / 2
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function expandBbox(bbox, padding) {
|
|
232
|
+
return {
|
|
233
|
+
x: bbox.x - padding,
|
|
234
|
+
y: bbox.y - padding,
|
|
235
|
+
width: bbox.width + padding * 2,
|
|
236
|
+
height: bbox.height + padding * 2
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function bboxFromPoints(x1, y1, x2, y2) {
|
|
240
|
+
return {
|
|
241
|
+
x: Math.min(x1, x2),
|
|
242
|
+
y: Math.min(y1, y2),
|
|
243
|
+
width: Math.abs(x2 - x1),
|
|
244
|
+
height: Math.abs(y2 - y1)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/utils/element-identification.ts
|
|
249
|
+
function generateSelector(element) {
|
|
250
|
+
if (element.id) {
|
|
251
|
+
return `#${element.id}`;
|
|
252
|
+
}
|
|
253
|
+
const path = [];
|
|
254
|
+
let current = element;
|
|
255
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
256
|
+
let selector = current.tagName.toLowerCase();
|
|
257
|
+
if (current.id) {
|
|
258
|
+
selector = `#${current.id}`;
|
|
259
|
+
path.unshift(selector);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (current.className && typeof current.className === "string") {
|
|
263
|
+
const meaningfulClasses = current.className.split(/\s+/).filter((c) => c.length > 2 && !c.match(/^[a-z]{1,2}$/) && !c.match(/[A-Z0-9]{5,}/)).slice(0, 2);
|
|
264
|
+
if (meaningfulClasses.length > 0) {
|
|
265
|
+
selector += "." + meaningfulClasses.join(".");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const parent = current.parentElement;
|
|
269
|
+
if (parent) {
|
|
270
|
+
const siblings = Array.from(parent.children).filter(
|
|
271
|
+
(child) => child.tagName === current.tagName
|
|
272
|
+
);
|
|
273
|
+
if (siblings.length > 1) {
|
|
274
|
+
const index = siblings.indexOf(current) + 1;
|
|
275
|
+
selector += `:nth-child(${index})`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
path.unshift(selector);
|
|
279
|
+
current = current.parentElement;
|
|
280
|
+
}
|
|
281
|
+
return path.join(" > ");
|
|
282
|
+
}
|
|
283
|
+
function getElementPath(target, maxDepth = 4) {
|
|
284
|
+
const parts = [];
|
|
285
|
+
let current = target;
|
|
286
|
+
let depth = 0;
|
|
287
|
+
while (current && depth < maxDepth) {
|
|
288
|
+
const tag = current.tagName.toLowerCase();
|
|
289
|
+
if (tag === "html" || tag === "body") break;
|
|
290
|
+
let identifier = tag;
|
|
291
|
+
if (current.id) {
|
|
292
|
+
identifier = `#${current.id}`;
|
|
293
|
+
} else if (current.className && typeof current.className === "string") {
|
|
294
|
+
const meaningfulClass = current.className.split(/\s+/).find((c) => c.length > 2 && !c.match(/^[a-z]{1,2}$/) && !c.match(/[A-Z0-9]{5,}/));
|
|
295
|
+
if (meaningfulClass) {
|
|
296
|
+
identifier = `.${meaningfulClass.split("_")[0]}`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
parts.unshift(identifier);
|
|
300
|
+
current = current.parentElement;
|
|
301
|
+
depth++;
|
|
302
|
+
}
|
|
303
|
+
return parts.join(" > ");
|
|
304
|
+
}
|
|
305
|
+
function identifyElement(target) {
|
|
306
|
+
const tag = target.tagName.toLowerCase();
|
|
307
|
+
if (tag === "button") {
|
|
308
|
+
const text = target.textContent?.trim();
|
|
309
|
+
const ariaLabel = target.getAttribute("aria-label");
|
|
310
|
+
if (ariaLabel) return `button [${ariaLabel}]`;
|
|
311
|
+
return text ? `button "${text.slice(0, 25)}"` : "button";
|
|
312
|
+
}
|
|
313
|
+
if (tag === "a") {
|
|
314
|
+
const text = target.textContent?.trim();
|
|
315
|
+
const href = target.getAttribute("href");
|
|
316
|
+
if (text) return `link "${text.slice(0, 25)}"`;
|
|
317
|
+
if (href) return `link to ${href.slice(0, 30)}`;
|
|
318
|
+
return "link";
|
|
319
|
+
}
|
|
320
|
+
if (tag === "input") {
|
|
321
|
+
const type = target.getAttribute("type") || "text";
|
|
322
|
+
const placeholder = target.getAttribute("placeholder");
|
|
323
|
+
const name = target.getAttribute("name");
|
|
324
|
+
if (placeholder) return `input "${placeholder}"`;
|
|
325
|
+
if (name) return `input [${name}]`;
|
|
326
|
+
return `${type} input`;
|
|
327
|
+
}
|
|
328
|
+
if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
|
|
329
|
+
const text = target.textContent?.trim();
|
|
330
|
+
return text ? `${tag} "${text.slice(0, 35)}"` : tag;
|
|
331
|
+
}
|
|
332
|
+
if (tag === "p") {
|
|
333
|
+
const text = target.textContent?.trim();
|
|
334
|
+
if (text) return `paragraph: "${text.slice(0, 40)}${text.length > 40 ? "..." : ""}"`;
|
|
335
|
+
return "paragraph";
|
|
336
|
+
}
|
|
337
|
+
if (tag === "img") {
|
|
338
|
+
const alt = target.getAttribute("alt");
|
|
339
|
+
return alt ? `image "${alt.slice(0, 30)}"` : "image";
|
|
340
|
+
}
|
|
341
|
+
if (["div", "section", "article", "nav", "header", "footer", "aside", "main"].includes(tag)) {
|
|
342
|
+
const className = target.className;
|
|
343
|
+
const role = target.getAttribute("role");
|
|
344
|
+
const ariaLabel = target.getAttribute("aria-label");
|
|
345
|
+
if (ariaLabel) return `${tag} [${ariaLabel}]`;
|
|
346
|
+
if (role) return role;
|
|
347
|
+
if (typeof className === "string" && className) {
|
|
348
|
+
const words = className.split(/[\s_-]+/).map((c) => c.replace(/[A-Z0-9]{5,}.*$/, "")).filter((c) => c.length > 2 && !/^[a-z]{1,2}$/.test(c)).slice(0, 2);
|
|
349
|
+
if (words.length > 0) return words.join(" ");
|
|
350
|
+
}
|
|
351
|
+
return tag === "div" ? "container" : tag;
|
|
352
|
+
}
|
|
353
|
+
return tag;
|
|
354
|
+
}
|
|
355
|
+
function getBoundingBox(element) {
|
|
356
|
+
const rect = element.getBoundingClientRect();
|
|
357
|
+
return {
|
|
358
|
+
x: rect.left + window.scrollX,
|
|
359
|
+
y: rect.top + window.scrollY,
|
|
360
|
+
width: rect.width,
|
|
361
|
+
height: rect.height
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function getElementClasses(target) {
|
|
365
|
+
const className = target.className;
|
|
366
|
+
if (typeof className !== "string" || !className) return "";
|
|
367
|
+
const classes = className.split(/\s+/).filter((c) => c.length > 0).map((c) => {
|
|
368
|
+
const match = c.match(/^([a-zA-Z][a-zA-Z0-9_-]*?)(?:_[a-zA-Z0-9]{5,})?$/);
|
|
369
|
+
return match ? match[1] : c;
|
|
370
|
+
}).filter((c, i, arr) => arr.indexOf(c) === i);
|
|
371
|
+
return classes.join(", ");
|
|
372
|
+
}
|
|
373
|
+
function createDOMSelection(element) {
|
|
374
|
+
return {
|
|
375
|
+
id: `dom-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
376
|
+
selector: generateSelector(element),
|
|
377
|
+
tagName: element.tagName.toLowerCase(),
|
|
378
|
+
elementPath: getElementPath(element),
|
|
379
|
+
text: element.textContent?.trim().slice(0, 100) || "",
|
|
380
|
+
boundingBox: getBoundingBox(element),
|
|
381
|
+
timestamp: Date.now(),
|
|
382
|
+
pathname: typeof window !== "undefined" ? window.location.pathname : "",
|
|
383
|
+
cssClasses: getElementClasses(element) || void 0,
|
|
384
|
+
attributes: getElementAttributes(element)
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function getElementAttributes(element) {
|
|
388
|
+
const attrs = {};
|
|
389
|
+
const relevantAttrs = ["id", "href", "src", "alt", "type", "name", "placeholder", "role", "aria-label"];
|
|
390
|
+
for (const attr of relevantAttrs) {
|
|
391
|
+
const value = element.getAttribute(attr);
|
|
392
|
+
if (value) {
|
|
393
|
+
attrs[attr] = value;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return Object.keys(attrs).length > 0 ? attrs : void 0;
|
|
397
|
+
}
|
|
398
|
+
function shouldIgnoreElement(element) {
|
|
399
|
+
if (element.closest("[data-skema]")) return true;
|
|
400
|
+
if (element.closest(".tl-container")) return true;
|
|
401
|
+
const tag = element.tagName.toLowerCase();
|
|
402
|
+
if (["script", "style", "noscript", "meta", "link"].includes(tag)) return true;
|
|
403
|
+
if (tag === "section" || tag === "main" || tag === "body" || tag === "html") return true;
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
function getComputedElementStyles(element) {
|
|
407
|
+
const computed = window.getComputedStyle(element);
|
|
408
|
+
return {
|
|
409
|
+
// Typography
|
|
410
|
+
fontFamily: computed.fontFamily,
|
|
411
|
+
fontSize: computed.fontSize,
|
|
412
|
+
fontWeight: computed.fontWeight,
|
|
413
|
+
lineHeight: computed.lineHeight,
|
|
414
|
+
letterSpacing: computed.letterSpacing !== "normal" ? computed.letterSpacing : void 0,
|
|
415
|
+
textAlign: computed.textAlign !== "start" ? computed.textAlign : void 0,
|
|
416
|
+
color: computed.color,
|
|
417
|
+
// Spacing
|
|
418
|
+
padding: computed.padding !== "0px" ? computed.padding : void 0,
|
|
419
|
+
margin: computed.margin !== "0px" ? computed.margin : void 0,
|
|
420
|
+
gap: computed.gap !== "normal" ? computed.gap : void 0,
|
|
421
|
+
// Layout
|
|
422
|
+
display: computed.display,
|
|
423
|
+
flexDirection: computed.flexDirection !== "row" ? computed.flexDirection : void 0,
|
|
424
|
+
alignItems: computed.alignItems !== "normal" ? computed.alignItems : void 0,
|
|
425
|
+
justifyContent: computed.justifyContent !== "normal" ? computed.justifyContent : void 0,
|
|
426
|
+
// Visual
|
|
427
|
+
backgroundColor: computed.backgroundColor !== "rgba(0, 0, 0, 0)" ? computed.backgroundColor : void 0,
|
|
428
|
+
borderRadius: computed.borderRadius !== "0px" ? computed.borderRadius : void 0,
|
|
429
|
+
border: computed.border !== "none" && computed.borderWidth !== "0px" ? computed.border : void 0,
|
|
430
|
+
boxShadow: computed.boxShadow !== "none" ? computed.boxShadow : void 0,
|
|
431
|
+
// Sizing
|
|
432
|
+
width: computed.width,
|
|
433
|
+
height: computed.height,
|
|
434
|
+
maxWidth: computed.maxWidth !== "none" ? computed.maxWidth : void 0
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function extractTailwindClasses(element) {
|
|
438
|
+
const className = element.className;
|
|
439
|
+
if (typeof className !== "string" || !className) return void 0;
|
|
440
|
+
const tailwindPatterns = [
|
|
441
|
+
/^(flex|grid|block|inline|hidden)$/,
|
|
442
|
+
/^(flex|grid|items|justify|gap|space|p|m|px|py|mx|my|pt|pb|pl|pr|mt|mb|ml|mr)-/,
|
|
443
|
+
/^(w|h|min-w|min-h|max-w|max-h)-/,
|
|
444
|
+
/^(text|font|leading|tracking|bg|border|rounded|shadow|opacity)-/,
|
|
445
|
+
/^(sm|md|lg|xl|2xl):/,
|
|
446
|
+
/^(hover|focus|active|disabled):/,
|
|
447
|
+
/^(dark|light):/
|
|
448
|
+
];
|
|
449
|
+
const classes = className.split(/\s+/).filter(
|
|
450
|
+
(c) => tailwindPatterns.some((pattern) => pattern.test(c))
|
|
451
|
+
);
|
|
452
|
+
return classes.length > 0 ? classes : void 0;
|
|
453
|
+
}
|
|
454
|
+
function createNearbyElement(element) {
|
|
455
|
+
return {
|
|
456
|
+
selector: generateSelector(element),
|
|
457
|
+
tagName: element.tagName.toLowerCase(),
|
|
458
|
+
text: element.textContent?.trim().slice(0, 100),
|
|
459
|
+
styles: getComputedElementStyles(element),
|
|
460
|
+
tailwindClasses: extractTailwindClasses(element)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function detectCSSFramework() {
|
|
464
|
+
const hasTailwind = document.querySelector('[class*="flex"]') !== null || document.querySelector('[class*="grid-"]') !== null || document.querySelector('[class*="text-"]') !== null || document.querySelector('[class*="bg-"]') !== null || // Check for Tailwind's reset stylesheet
|
|
465
|
+
document.querySelector("style[data-precedence]") !== null;
|
|
466
|
+
if (hasTailwind) {
|
|
467
|
+
const allClasses = Array.from(document.querySelectorAll("[class]")).slice(0, 50).flatMap((el) => {
|
|
468
|
+
const className = el.className;
|
|
469
|
+
if (typeof className !== "string") return [];
|
|
470
|
+
return className.split(/\s+/);
|
|
471
|
+
});
|
|
472
|
+
const tailwindIndicators = allClasses.filter(
|
|
473
|
+
(c) => /^(flex|grid|block|inline-block|hidden|p-|m-|px-|py-|mx-|my-|w-|h-|text-|bg-|border-|rounded-|shadow-|gap-)/.test(c)
|
|
474
|
+
);
|
|
475
|
+
if (tailwindIndicators.length > 5) {
|
|
476
|
+
return "tailwind";
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const hasModuleClasses = Array.from(document.querySelectorAll("[class]")).slice(0, 20).some((el) => {
|
|
480
|
+
const className = el.className;
|
|
481
|
+
if (typeof className !== "string") return false;
|
|
482
|
+
return /[a-zA-Z]+_[a-zA-Z0-9]{5,}/.test(className);
|
|
483
|
+
});
|
|
484
|
+
if (hasModuleClasses) {
|
|
485
|
+
return "css-modules";
|
|
486
|
+
}
|
|
487
|
+
if (document.querySelector("[data-styled]") !== null || document.querySelector("style[data-styled-components]") !== null) {
|
|
488
|
+
return "styled-components";
|
|
489
|
+
}
|
|
490
|
+
return "vanilla";
|
|
491
|
+
}
|
|
492
|
+
function extractCSSVariables() {
|
|
493
|
+
const variables = {};
|
|
494
|
+
try {
|
|
495
|
+
const rootStyles = getComputedStyle(document.documentElement);
|
|
496
|
+
for (const sheet of document.styleSheets) {
|
|
497
|
+
try {
|
|
498
|
+
const rules = sheet.cssRules || sheet.rules;
|
|
499
|
+
for (const rule of rules) {
|
|
500
|
+
if (rule instanceof CSSStyleRule && rule.selectorText === ":root") {
|
|
501
|
+
for (let i = 0; i < rule.style.length; i++) {
|
|
502
|
+
const prop = rule.style[i];
|
|
503
|
+
if (prop.startsWith("--")) {
|
|
504
|
+
variables[prop] = rule.style.getPropertyValue(prop).trim();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const htmlStyle = document.documentElement.getAttribute("style");
|
|
513
|
+
if (htmlStyle) {
|
|
514
|
+
const matches = htmlStyle.matchAll(/--([a-zA-Z0-9-]+)\s*:\s*([^;]+)/g);
|
|
515
|
+
for (const match of matches) {
|
|
516
|
+
variables[`--${match[1]}`] = match[2].trim();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
return Object.keys(variables).length > 0 ? variables : void 0;
|
|
522
|
+
}
|
|
523
|
+
function extractColorPalette() {
|
|
524
|
+
const colors = /* @__PURE__ */ new Set();
|
|
525
|
+
const elements = document.querySelectorAll("*");
|
|
526
|
+
const sampleSize = Math.min(elements.length, 100);
|
|
527
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
528
|
+
const el = elements[i];
|
|
529
|
+
if (shouldIgnoreElement(el)) continue;
|
|
530
|
+
const computed = window.getComputedStyle(el);
|
|
531
|
+
if (computed.backgroundColor && computed.backgroundColor !== "rgba(0, 0, 0, 0)") {
|
|
532
|
+
colors.add(computed.backgroundColor);
|
|
533
|
+
}
|
|
534
|
+
if (computed.color) {
|
|
535
|
+
colors.add(computed.color);
|
|
536
|
+
}
|
|
537
|
+
if (computed.borderColor && computed.borderColor !== "rgb(0, 0, 0)") {
|
|
538
|
+
colors.add(computed.borderColor);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const uniqueColors = Array.from(colors).slice(0, 20);
|
|
542
|
+
return uniqueColors.length > 0 ? uniqueColors : void 0;
|
|
543
|
+
}
|
|
544
|
+
function extractProjectStyleContext() {
|
|
545
|
+
const bodyStyles = window.getComputedStyle(document.body);
|
|
546
|
+
const htmlStyles = window.getComputedStyle(document.documentElement);
|
|
547
|
+
return {
|
|
548
|
+
cssFramework: detectCSSFramework(),
|
|
549
|
+
cssVariables: extractCSSVariables(),
|
|
550
|
+
colorPalette: extractColorPalette(),
|
|
551
|
+
baseFontFamily: bodyStyles.fontFamily,
|
|
552
|
+
baseFontSize: htmlStyles.fontSize
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function findNearbyElementsWithStyles(bounds, maxElements = 5) {
|
|
556
|
+
const elements = [];
|
|
557
|
+
const allElements = document.querySelectorAll("*");
|
|
558
|
+
const boundsCenter = {
|
|
559
|
+
x: bounds.x + bounds.width / 2,
|
|
560
|
+
y: bounds.y + bounds.height / 2
|
|
561
|
+
};
|
|
562
|
+
allElements.forEach((el) => {
|
|
563
|
+
if (!(el instanceof HTMLElement)) return;
|
|
564
|
+
if (shouldIgnoreElement(el)) return;
|
|
565
|
+
const rect = el.getBoundingClientRect();
|
|
566
|
+
if (rect.width < 10 || rect.height < 10) return;
|
|
567
|
+
const elCenter = {
|
|
568
|
+
x: rect.left + window.scrollX + rect.width / 2,
|
|
569
|
+
y: rect.top + window.scrollY + rect.height / 2
|
|
570
|
+
};
|
|
571
|
+
const distance = Math.sqrt(
|
|
572
|
+
Math.pow(boundsCenter.x - elCenter.x, 2) + Math.pow(boundsCenter.y - elCenter.y, 2)
|
|
573
|
+
);
|
|
574
|
+
if (distance < 500) {
|
|
575
|
+
elements.push({ element: el, distance });
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
elements.sort((a, b) => a.distance - b.distance);
|
|
579
|
+
const seen = /* @__PURE__ */ new Set();
|
|
580
|
+
const diverse = [];
|
|
581
|
+
for (const { element } of elements) {
|
|
582
|
+
const key = `${element.tagName}-${element.className}`;
|
|
583
|
+
if (!seen.has(key)) {
|
|
584
|
+
seen.add(key);
|
|
585
|
+
diverse.push(element);
|
|
586
|
+
if (diverse.length >= maxElements) break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return diverse.map(createNearbyElement);
|
|
590
|
+
}
|
|
591
|
+
var styles = {
|
|
592
|
+
popup: {
|
|
593
|
+
position: "fixed",
|
|
594
|
+
transform: "translateX(-50%)",
|
|
595
|
+
width: 280,
|
|
596
|
+
padding: "12px 16px 14px",
|
|
597
|
+
background: "#1a1a1a",
|
|
598
|
+
borderRadius: 16,
|
|
599
|
+
boxShadow: "0 4px 24px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.08)",
|
|
600
|
+
cursor: "default",
|
|
601
|
+
zIndex: 100001,
|
|
602
|
+
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
603
|
+
opacity: 0,
|
|
604
|
+
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
605
|
+
},
|
|
606
|
+
popupEnter: {
|
|
607
|
+
opacity: 1,
|
|
608
|
+
transform: "translateX(-50%) scale(1) translateY(0)"
|
|
609
|
+
},
|
|
610
|
+
popupExit: {
|
|
611
|
+
opacity: 0,
|
|
612
|
+
transform: "translateX(-50%) scale(0.95) translateY(4px)"
|
|
613
|
+
},
|
|
614
|
+
header: {
|
|
615
|
+
display: "flex",
|
|
616
|
+
alignItems: "center",
|
|
617
|
+
justifyContent: "space-between",
|
|
618
|
+
marginBottom: 9
|
|
619
|
+
},
|
|
620
|
+
element: {
|
|
621
|
+
fontSize: 12,
|
|
622
|
+
fontWeight: 400,
|
|
623
|
+
color: "rgba(255, 255, 255, 0.5)",
|
|
624
|
+
maxWidth: "100%",
|
|
625
|
+
overflow: "hidden",
|
|
626
|
+
textOverflow: "ellipsis",
|
|
627
|
+
whiteSpace: "nowrap",
|
|
628
|
+
flex: 1
|
|
629
|
+
},
|
|
630
|
+
quote: {
|
|
631
|
+
fontSize: 12,
|
|
632
|
+
fontStyle: "italic",
|
|
633
|
+
color: "rgba(255, 255, 255, 0.6)",
|
|
634
|
+
marginBottom: 8,
|
|
635
|
+
padding: "6px 8px",
|
|
636
|
+
background: "rgba(255, 255, 255, 0.05)",
|
|
637
|
+
borderRadius: 4,
|
|
638
|
+
lineHeight: 1.45
|
|
639
|
+
},
|
|
640
|
+
textarea: {
|
|
641
|
+
width: "100%",
|
|
642
|
+
padding: "8px 10px",
|
|
643
|
+
fontSize: 13,
|
|
644
|
+
fontFamily: "inherit",
|
|
645
|
+
background: "rgba(255, 255, 255, 0.05)",
|
|
646
|
+
color: "#fff",
|
|
647
|
+
border: "1px solid rgba(255, 255, 255, 0.15)",
|
|
648
|
+
borderRadius: 8,
|
|
649
|
+
resize: "none",
|
|
650
|
+
outline: "none",
|
|
651
|
+
transition: "border-color 0.15s ease",
|
|
652
|
+
boxSizing: "border-box"
|
|
653
|
+
},
|
|
654
|
+
actions: {
|
|
655
|
+
display: "flex",
|
|
656
|
+
justifyContent: "flex-end",
|
|
657
|
+
gap: 6,
|
|
658
|
+
marginTop: 8
|
|
659
|
+
},
|
|
660
|
+
button: {
|
|
661
|
+
padding: "6px 14px",
|
|
662
|
+
fontSize: 12,
|
|
663
|
+
fontWeight: 500,
|
|
664
|
+
borderRadius: 16,
|
|
665
|
+
border: "none",
|
|
666
|
+
cursor: "pointer",
|
|
667
|
+
transition: "background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease"
|
|
668
|
+
},
|
|
669
|
+
cancelButton: {
|
|
670
|
+
background: "transparent",
|
|
671
|
+
color: "rgba(255, 255, 255, 0.5)"
|
|
672
|
+
},
|
|
673
|
+
submitButton: {
|
|
674
|
+
color: "white"
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var AnnotationPopup = forwardRef(
|
|
678
|
+
function AnnotationPopup2({
|
|
679
|
+
element,
|
|
680
|
+
selectedText,
|
|
681
|
+
placeholder = "What should change?",
|
|
682
|
+
initialValue = "",
|
|
683
|
+
submitLabel = "Add",
|
|
684
|
+
onSubmit,
|
|
685
|
+
onCancel,
|
|
686
|
+
style,
|
|
687
|
+
accentColor = "#3c82f7",
|
|
688
|
+
isExiting = false,
|
|
689
|
+
isMultiSelect = false
|
|
690
|
+
}, ref) {
|
|
691
|
+
const [text, setText] = useState(initialValue);
|
|
692
|
+
const [isShaking, setIsShaking] = useState(false);
|
|
693
|
+
const [animState, setAnimState] = useState("initial");
|
|
694
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
695
|
+
const textareaRef = useRef(null);
|
|
696
|
+
const popupRef = useRef(null);
|
|
697
|
+
useEffect(() => {
|
|
698
|
+
if (isExiting && animState !== "exit") {
|
|
699
|
+
setAnimState("exit");
|
|
700
|
+
}
|
|
701
|
+
}, [isExiting, animState]);
|
|
702
|
+
useEffect(() => {
|
|
703
|
+
requestAnimationFrame(() => {
|
|
704
|
+
setAnimState("enter");
|
|
705
|
+
});
|
|
706
|
+
const enterTimer = setTimeout(() => {
|
|
707
|
+
setAnimState("entered");
|
|
708
|
+
}, 200);
|
|
709
|
+
const focusTimer = setTimeout(() => {
|
|
710
|
+
const textarea = textareaRef.current;
|
|
711
|
+
if (textarea) {
|
|
712
|
+
textarea.focus();
|
|
713
|
+
textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
|
|
714
|
+
textarea.scrollTop = textarea.scrollHeight;
|
|
715
|
+
}
|
|
716
|
+
}, 50);
|
|
717
|
+
return () => {
|
|
718
|
+
clearTimeout(enterTimer);
|
|
719
|
+
clearTimeout(focusTimer);
|
|
720
|
+
};
|
|
721
|
+
}, []);
|
|
722
|
+
const shake = useCallback(() => {
|
|
723
|
+
setIsShaking(true);
|
|
724
|
+
setTimeout(() => {
|
|
725
|
+
setIsShaking(false);
|
|
726
|
+
textareaRef.current?.focus();
|
|
727
|
+
}, 250);
|
|
728
|
+
}, []);
|
|
729
|
+
useImperativeHandle(ref, () => ({
|
|
730
|
+
shake
|
|
731
|
+
}), [shake]);
|
|
732
|
+
const handleCancel = useCallback(() => {
|
|
733
|
+
setAnimState("exit");
|
|
734
|
+
setTimeout(() => {
|
|
735
|
+
onCancel();
|
|
736
|
+
}, 150);
|
|
737
|
+
}, [onCancel]);
|
|
738
|
+
const handleSubmit = useCallback(() => {
|
|
739
|
+
if (!text.trim()) return;
|
|
740
|
+
onSubmit(text.trim());
|
|
741
|
+
}, [text, onSubmit]);
|
|
742
|
+
const handleKeyDown = useCallback(
|
|
743
|
+
(e) => {
|
|
744
|
+
if (e.nativeEvent.isComposing) return;
|
|
745
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
746
|
+
e.preventDefault();
|
|
747
|
+
handleSubmit();
|
|
748
|
+
}
|
|
749
|
+
if (e.key === "Escape") {
|
|
750
|
+
handleCancel();
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
[handleSubmit, handleCancel]
|
|
754
|
+
);
|
|
755
|
+
const popupStyle = {
|
|
756
|
+
...styles.popup,
|
|
757
|
+
...animState === "enter" || animState === "entered" ? styles.popupEnter : {},
|
|
758
|
+
...animState === "exit" ? styles.popupExit : {},
|
|
759
|
+
...isShaking ? {
|
|
760
|
+
animation: "skema-shake 0.25s ease-out"
|
|
761
|
+
} : {},
|
|
762
|
+
...style
|
|
763
|
+
};
|
|
764
|
+
const textareaStyle = {
|
|
765
|
+
...styles.textarea,
|
|
766
|
+
...isFocused ? { borderColor: accentColor } : {}
|
|
767
|
+
};
|
|
768
|
+
const effectiveAccentColor = isMultiSelect ? "#34C759" : accentColor;
|
|
769
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
770
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
771
|
+
@keyframes skema-shake {
|
|
772
|
+
0%, 100% { transform: translateX(-50%) scale(1) translateY(0) translateX(0); }
|
|
773
|
+
20% { transform: translateX(-50%) scale(1) translateY(0) translateX(-3px); }
|
|
774
|
+
40% { transform: translateX(-50%) scale(1) translateY(0) translateX(3px); }
|
|
775
|
+
60% { transform: translateX(-50%) scale(1) translateY(0) translateX(-2px); }
|
|
776
|
+
80% { transform: translateX(-50%) scale(1) translateY(0) translateX(2px); }
|
|
777
|
+
}
|
|
778
|
+
` }),
|
|
779
|
+
/* @__PURE__ */ jsxs(
|
|
780
|
+
"div",
|
|
781
|
+
{
|
|
782
|
+
ref: popupRef,
|
|
783
|
+
"data-skema": "annotation-popup",
|
|
784
|
+
style: popupStyle,
|
|
785
|
+
onClick: (e) => e.stopPropagation(),
|
|
786
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
787
|
+
children: [
|
|
788
|
+
/* @__PURE__ */ jsx("div", { style: styles.header, children: /* @__PURE__ */ jsx("span", { style: styles.element, children: element }) }),
|
|
789
|
+
selectedText && /* @__PURE__ */ jsxs("div", { style: styles.quote, children: [
|
|
790
|
+
"\u201C",
|
|
791
|
+
selectedText.slice(0, 80),
|
|
792
|
+
selectedText.length > 80 ? "..." : "",
|
|
793
|
+
"\u201D"
|
|
794
|
+
] }),
|
|
795
|
+
/* @__PURE__ */ jsx(
|
|
796
|
+
"textarea",
|
|
797
|
+
{
|
|
798
|
+
ref: textareaRef,
|
|
799
|
+
style: textareaStyle,
|
|
800
|
+
placeholder,
|
|
801
|
+
value: text,
|
|
802
|
+
onChange: (e) => setText(e.target.value),
|
|
803
|
+
onFocus: () => setIsFocused(true),
|
|
804
|
+
onBlur: () => setIsFocused(false),
|
|
805
|
+
rows: 2,
|
|
806
|
+
onKeyDown: handleKeyDown
|
|
807
|
+
}
|
|
808
|
+
),
|
|
809
|
+
/* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
|
|
810
|
+
/* @__PURE__ */ jsx(
|
|
811
|
+
"button",
|
|
812
|
+
{
|
|
813
|
+
style: { ...styles.button, ...styles.cancelButton },
|
|
814
|
+
onClick: handleCancel,
|
|
815
|
+
onMouseEnter: (e) => {
|
|
816
|
+
e.currentTarget.style.background = "rgba(255, 255, 255, 0.1)";
|
|
817
|
+
e.currentTarget.style.color = "rgba(255, 255, 255, 0.8)";
|
|
818
|
+
},
|
|
819
|
+
onMouseLeave: (e) => {
|
|
820
|
+
e.currentTarget.style.background = "transparent";
|
|
821
|
+
e.currentTarget.style.color = "rgba(255, 255, 255, 0.5)";
|
|
822
|
+
},
|
|
823
|
+
children: "Cancel"
|
|
824
|
+
}
|
|
825
|
+
),
|
|
826
|
+
/* @__PURE__ */ jsx(
|
|
827
|
+
"button",
|
|
828
|
+
{
|
|
829
|
+
style: {
|
|
830
|
+
...styles.button,
|
|
831
|
+
...styles.submitButton,
|
|
832
|
+
backgroundColor: effectiveAccentColor,
|
|
833
|
+
opacity: text.trim() ? 1 : 0.4
|
|
834
|
+
},
|
|
835
|
+
onClick: handleSubmit,
|
|
836
|
+
disabled: !text.trim(),
|
|
837
|
+
onMouseEnter: (e) => {
|
|
838
|
+
if (text.trim()) {
|
|
839
|
+
e.currentTarget.style.filter = "brightness(0.9)";
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
onMouseLeave: (e) => {
|
|
843
|
+
e.currentTarget.style.filter = "none";
|
|
844
|
+
},
|
|
845
|
+
children: submitLabel
|
|
846
|
+
}
|
|
847
|
+
)
|
|
848
|
+
] })
|
|
849
|
+
]
|
|
850
|
+
}
|
|
851
|
+
)
|
|
852
|
+
] });
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
// src/lib/utils.ts
|
|
857
|
+
async function blobToBase64(blob) {
|
|
858
|
+
return new Promise((resolve, reject) => {
|
|
859
|
+
const reader = new FileReader();
|
|
860
|
+
reader.onload = () => {
|
|
861
|
+
const result = reader.result;
|
|
862
|
+
resolve(result);
|
|
863
|
+
};
|
|
864
|
+
reader.onerror = () => reject(reader.error);
|
|
865
|
+
reader.readAsDataURL(blob);
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function addGridToSvg(svgString, opts = {}) {
|
|
869
|
+
const { color = "#0066FF", size = 100, labels = true } = opts;
|
|
870
|
+
const viewBoxMatch = svgString.match(/viewBox="([^"]+)"/);
|
|
871
|
+
if (!viewBoxMatch) return svgString;
|
|
872
|
+
const [x, y, w, h] = viewBoxMatch[1].split(" ").map(Number);
|
|
873
|
+
const gridElements = [];
|
|
874
|
+
for (let i = 0; i <= Math.ceil(w / size); i++) {
|
|
875
|
+
const xPos = i * size;
|
|
876
|
+
if (i > 0) {
|
|
877
|
+
gridElements.push(
|
|
878
|
+
`<line x1="${xPos}" y1="0" x2="${xPos}" y2="${h}" stroke="${color}" stroke-width="1" stroke-opacity="0.5"/>`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (labels) {
|
|
882
|
+
const colLabel = String.fromCharCode(65 + i);
|
|
883
|
+
gridElements.push(
|
|
884
|
+
`<text x="${xPos + size / 2}" y="16" fill="${color}" font-size="12" font-family="sans-serif" text-anchor="middle">${colLabel}</text>`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
for (let i = 0; i <= Math.ceil(h / size); i++) {
|
|
889
|
+
const yPos = i * size;
|
|
890
|
+
gridElements.push(
|
|
891
|
+
`<line x1="0" y1="${yPos}" x2="${w}" y2="${yPos}" stroke="${color}" stroke-width="1" stroke-opacity="0.5"/>`
|
|
892
|
+
);
|
|
893
|
+
if (labels && i < Math.ceil(h / size)) {
|
|
894
|
+
gridElements.push(
|
|
895
|
+
`<text x="8" y="${yPos + size / 2 + 4}" fill="${color}" font-size="12" font-family="sans-serif">${i}</text>`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const gridGroup = `<g id="skema-grid" transform="translate(${x}, ${y})">${gridElements.join("")}</g>`;
|
|
900
|
+
return svgString.replace("</svg>", `${gridGroup}</svg>`);
|
|
901
|
+
}
|
|
902
|
+
function getGridCellReference(x, y, gridSize = 100) {
|
|
903
|
+
const col = Math.floor(x / gridSize);
|
|
904
|
+
const row = Math.floor(y / gridSize);
|
|
905
|
+
const colLabel = String.fromCharCode(65 + col);
|
|
906
|
+
return `${colLabel}${row}`;
|
|
907
|
+
}
|
|
908
|
+
function extractTextFromShapes(shapes) {
|
|
909
|
+
const textContent = [];
|
|
910
|
+
for (const shape of shapes) {
|
|
911
|
+
const s = shape;
|
|
912
|
+
if (s.type === "text" || s.type === "note") {
|
|
913
|
+
if (s.props?.text) {
|
|
914
|
+
textContent.push(s.props.text);
|
|
915
|
+
}
|
|
916
|
+
if (s.props?.richText && typeof s.props.richText === "object") {
|
|
917
|
+
const rt = s.props.richText;
|
|
918
|
+
if (rt.content) {
|
|
919
|
+
for (const block of rt.content) {
|
|
920
|
+
if (block.content) {
|
|
921
|
+
for (const inline of block.content) {
|
|
922
|
+
if (inline.text) {
|
|
923
|
+
textContent.push(inline.text);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return textContent.filter(Boolean).join("\n");
|
|
933
|
+
}
|
|
934
|
+
var AnnotationMarker = ({
|
|
935
|
+
annotation,
|
|
936
|
+
index,
|
|
937
|
+
scrollOffset,
|
|
938
|
+
onHover,
|
|
939
|
+
onClick,
|
|
940
|
+
isHovered,
|
|
941
|
+
accentColor = "#3b82f6"
|
|
942
|
+
}) => {
|
|
943
|
+
let markerX;
|
|
944
|
+
let markerY;
|
|
945
|
+
if (annotation.type === "dom_selection") {
|
|
946
|
+
const sel = annotation;
|
|
947
|
+
markerX = sel.boundingBox.x + sel.boundingBox.width / 2;
|
|
948
|
+
markerY = sel.boundingBox.y;
|
|
949
|
+
} else if (annotation.type === "drawing") {
|
|
950
|
+
markerX = annotation.boundingBox.x + annotation.boundingBox.width / 2;
|
|
951
|
+
markerY = annotation.boundingBox.y;
|
|
952
|
+
} else {
|
|
953
|
+
markerX = annotation.boundingBox.x + annotation.boundingBox.width / 2;
|
|
954
|
+
markerY = annotation.boundingBox.y;
|
|
955
|
+
}
|
|
956
|
+
const viewportX = markerX - scrollOffset.x;
|
|
957
|
+
const viewportY = markerY - scrollOffset.y - 12;
|
|
958
|
+
const isDrawing = annotation.type === "drawing";
|
|
959
|
+
const markerColor = isDrawing ? "#8B5CF6" : accentColor;
|
|
960
|
+
const comment = annotation.type === "dom_selection" ? annotation.comment : void 0;
|
|
961
|
+
const elementName = annotation.type === "dom_selection" ? annotation.tagName : "Drawing";
|
|
962
|
+
return /* @__PURE__ */ jsxs(
|
|
963
|
+
"div",
|
|
964
|
+
{
|
|
965
|
+
"data-skema": "annotation-marker",
|
|
966
|
+
style: {
|
|
967
|
+
position: "fixed",
|
|
968
|
+
left: viewportX,
|
|
969
|
+
top: viewportY,
|
|
970
|
+
transform: "translate(-50%, -100%)",
|
|
971
|
+
zIndex: 999998,
|
|
972
|
+
pointerEvents: "auto"
|
|
973
|
+
},
|
|
974
|
+
children: [
|
|
975
|
+
/* @__PURE__ */ jsx(
|
|
976
|
+
"div",
|
|
977
|
+
{
|
|
978
|
+
style: {
|
|
979
|
+
width: isHovered ? 26 : 22,
|
|
980
|
+
height: isHovered ? 26 : 22,
|
|
981
|
+
borderRadius: "50%",
|
|
982
|
+
backgroundColor: isHovered ? "#ef4444" : markerColor,
|
|
983
|
+
color: "white",
|
|
984
|
+
display: "flex",
|
|
985
|
+
alignItems: "center",
|
|
986
|
+
justifyContent: "center",
|
|
987
|
+
fontSize: 11,
|
|
988
|
+
fontWeight: 600,
|
|
989
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
990
|
+
cursor: "pointer",
|
|
991
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
|
|
992
|
+
transition: "all 0.15s ease",
|
|
993
|
+
userSelect: "none"
|
|
994
|
+
},
|
|
995
|
+
onMouseEnter: () => onHover(annotation.id),
|
|
996
|
+
onMouseLeave: () => onHover(null),
|
|
997
|
+
onClick: (e) => {
|
|
998
|
+
e.stopPropagation();
|
|
999
|
+
onClick(annotation);
|
|
1000
|
+
},
|
|
1001
|
+
children: isHovered ? "\xD7" : index + 1
|
|
1002
|
+
}
|
|
1003
|
+
),
|
|
1004
|
+
isHovered && /* @__PURE__ */ jsxs(
|
|
1005
|
+
"div",
|
|
1006
|
+
{
|
|
1007
|
+
style: {
|
|
1008
|
+
position: "absolute",
|
|
1009
|
+
left: "50%",
|
|
1010
|
+
bottom: "100%",
|
|
1011
|
+
transform: "translateX(-50%)",
|
|
1012
|
+
marginBottom: 8,
|
|
1013
|
+
padding: "8px 12px",
|
|
1014
|
+
backgroundColor: "#1a1a1a",
|
|
1015
|
+
borderRadius: 8,
|
|
1016
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
|
1017
|
+
whiteSpace: "nowrap",
|
|
1018
|
+
maxWidth: 200,
|
|
1019
|
+
zIndex: 999999
|
|
1020
|
+
},
|
|
1021
|
+
children: [
|
|
1022
|
+
/* @__PURE__ */ jsx("div", { style: {
|
|
1023
|
+
fontSize: 11,
|
|
1024
|
+
color: "rgba(255,255,255,0.6)",
|
|
1025
|
+
marginBottom: comment ? 4 : 0
|
|
1026
|
+
}, children: elementName }),
|
|
1027
|
+
comment && /* @__PURE__ */ jsx("div", { style: {
|
|
1028
|
+
fontSize: 12,
|
|
1029
|
+
color: "white",
|
|
1030
|
+
whiteSpace: "normal",
|
|
1031
|
+
wordBreak: "break-word"
|
|
1032
|
+
}, children: comment.length > 50 ? comment.slice(0, 50) + "..." : comment }),
|
|
1033
|
+
/* @__PURE__ */ jsx("div", { style: {
|
|
1034
|
+
fontSize: 10,
|
|
1035
|
+
color: "rgba(255,255,255,0.4)",
|
|
1036
|
+
marginTop: 4
|
|
1037
|
+
}, children: "Click to delete" })
|
|
1038
|
+
]
|
|
1039
|
+
}
|
|
1040
|
+
)
|
|
1041
|
+
]
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
};
|
|
1045
|
+
var AnnotationMarkersLayer = ({ annotations, scrollOffset, hoveredMarkerId, onHover, onDelete }) => {
|
|
1046
|
+
return /* @__PURE__ */ jsx(Fragment, { children: annotations.map((annotation, index) => /* @__PURE__ */ jsx(
|
|
1047
|
+
AnnotationMarker,
|
|
1048
|
+
{
|
|
1049
|
+
annotation,
|
|
1050
|
+
index,
|
|
1051
|
+
scrollOffset,
|
|
1052
|
+
onHover,
|
|
1053
|
+
onClick: onDelete,
|
|
1054
|
+
isHovered: hoveredMarkerId === annotation.id
|
|
1055
|
+
},
|
|
1056
|
+
annotation.id
|
|
1057
|
+
)) });
|
|
1058
|
+
};
|
|
1059
|
+
var SelectIcon = ({ isSelected }) => /* @__PURE__ */ jsxs("svg", { width: "42", height: "42", viewBox: "0 0 25 25", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
1060
|
+
/* @__PURE__ */ jsx(
|
|
1061
|
+
"path",
|
|
1062
|
+
{
|
|
1063
|
+
d: "M11.268 3C12.0378 1.6667 13.9623 1.6667 14.7321 3L25.1244 21C25.8942 22.3333 24.9319 24 23.3923 24H2.6077C1.0681 24 0.1058 22.3333 0.8756 21L11.268 3Z",
|
|
1064
|
+
fill: "#F24E1E",
|
|
1065
|
+
opacity: isSelected ? 1 : 0.7
|
|
1066
|
+
}
|
|
1067
|
+
),
|
|
1068
|
+
/* @__PURE__ */ jsx(
|
|
1069
|
+
"path",
|
|
1070
|
+
{
|
|
1071
|
+
d: "M9 10L9 18.5L11.5 16L14 20L15.5 19L13 15L16 14.5L9 10Z",
|
|
1072
|
+
fill: "white"
|
|
1073
|
+
}
|
|
1074
|
+
)
|
|
1075
|
+
] });
|
|
1076
|
+
var DrawIcon = ({ isSelected }) => /* @__PURE__ */ jsxs("svg", { width: "42", height: "42", viewBox: "0 0 25 25", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
1077
|
+
/* @__PURE__ */ jsx("rect", { width: "25", height: "25", rx: "2", fill: isSelected ? "#00C851" : "#00C851", opacity: isSelected ? 1 : 0.7 }),
|
|
1078
|
+
/* @__PURE__ */ jsx("g", { transform: "translate(12.5, 12.5) scale(1.4) translate(-12.5, -12.5)", children: /* @__PURE__ */ jsx(
|
|
1079
|
+
"path",
|
|
1080
|
+
{
|
|
1081
|
+
fillRule: "evenodd",
|
|
1082
|
+
clipRule: "evenodd",
|
|
1083
|
+
d: "M13.6919 10.2852L14.2593 9.6908L14.8282 10.2864L14.2605 10.8808L13.6919 10.2852ZM9.5682 15.7944L8.9992 15.1988L13.1233 10.8808L13.6919 11.476L9.5682 15.7944ZM14.3284 8.5L8 15.1988V16.5H9.5682L16 10.0436L14.3284 8.5Z",
|
|
1084
|
+
fill: "white"
|
|
1085
|
+
}
|
|
1086
|
+
) })
|
|
1087
|
+
] });
|
|
1088
|
+
var LassoIcon = ({ isSelected }) => /* @__PURE__ */ jsxs("svg", { width: "42", height: "42", viewBox: "0 0 25 25", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
1089
|
+
/* @__PURE__ */ jsx("rect", { width: "25", height: "25", rx: "12.5", fill: isSelected ? "#2C7FFF" : "#2C7FFF", opacity: isSelected ? 1 : 0.7 }),
|
|
1090
|
+
/* @__PURE__ */ jsx("g", { transform: "translate(12.5, 12.5) scale(1.4) translate(-12.5, -12.5)", children: /* @__PURE__ */ jsx(
|
|
1091
|
+
"path",
|
|
1092
|
+
{
|
|
1093
|
+
fillRule: "evenodd",
|
|
1094
|
+
clipRule: "evenodd",
|
|
1095
|
+
d: "M9.219 11.3C9.219 10.8021 9.504 10.3117 10.043 9.9297C10.582 9.5484 11.347 9.3 12.211 9.3C13.074 9.3 13.839 9.5484 14.378 9.9297C14.918 10.3117 15.202 10.8021 15.202 11.3C15.202 11.7979 14.918 12.2882 14.378 12.6702C13.839 13.0515 13.074 13.2999 12.211 13.2999C12.005 13.2999 11.805 13.2859 11.612 13.2591C11.586 12.5417 10.887 12.0999 10.216 12.0999C9.988 12.0999 9.768 12.147 9.572 12.234C9.339 11.9444 9.219 11.625 9.219 11.3ZM12.211 14.0999C11.908 14.0999 11.614 14.074 11.331 14.0249C11.298 14.0629 11.262 14.0988 11.224 14.1325C11.226 14.154 11.228 14.1774 11.229 14.2026C11.232 14.3182 11.216 14.4769 11.137 14.6456C10.97 15.0066 10.586 15.2824 9.919 15.3874C9.091 15.5175 8.878 15.7607 8.827 15.8497C8.8 15.8961 8.797 15.9328 8.798 15.9542C8.798 15.9629 8.799 15.9695 8.8 15.973C8.805 15.9901 8.81 16.0079 8.813 16.026C8.833 16.1321 8.809 16.2395 8.751 16.3254C8.718 16.3733 8.675 16.4145 8.623 16.445C8.584 16.4681 8.54 16.4847 8.494 16.4932C8.447 16.502 8.4 16.5021 8.355 16.4944C8.296 16.4846 8.242 16.4619 8.195 16.4294C8.148 16.3972 8.108 16.3549 8.078 16.304C8.063 16.278 8.05 16.2501 8.041 16.2208C8.04 16.217 8.038 16.2128 8.037 16.2083C8.032 16.1931 8.027 16.1741 8.022 16.1517C8.012 16.1071 8.002 16.0477 8 15.977C7.996 15.8338 8.023 15.6452 8.136 15.4492C8.365 15.0533 8.87 14.7426 9.795 14.5971C9.958 14.5714 10.079 14.5362 10.167 14.4992C9.499 14.478 8.82 14.0237 8.82 13.2999C8.82 13.0999 8.876 12.9166 8.969 12.758C8.629 12.344 8.421 11.8466 8.421 11.3C8.421 10.4724 8.896 9.7627 9.583 9.2761C10.272 8.7888 11.202 8.5 12.211 8.5C13.219 8.5 14.15 8.7888 14.838 9.2761C15.526 9.7627 16 10.4724 16 11.3C16 12.1275 15.526 12.8372 14.838 13.3238C14.15 13.8111 13.219 14.0999 12.211 14.0999ZM9.754 13.0514C9.859 12.9649 10.021 12.8999 10.216 12.8999C10.634 12.8999 10.815 13.1577 10.815 13.2999C10.815 13.3371 10.806 13.3739 10.789 13.4108C10.757 13.4794 10.69 13.5552 10.58 13.6136C10.482 13.666 10.357 13.6999 10.216 13.6999C9.798 13.6999 9.618 13.4422 9.618 13.2999C9.618 13.2236 9.655 13.1338 9.754 13.0514Z",
|
|
1096
|
+
fill: "white"
|
|
1097
|
+
}
|
|
1098
|
+
) })
|
|
1099
|
+
] });
|
|
1100
|
+
var EraseIcon = ({ isSelected }) => /* @__PURE__ */ jsxs("svg", { width: "50", height: "42", viewBox: "0 0 30 25", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
1101
|
+
/* @__PURE__ */ jsx(
|
|
1102
|
+
"path",
|
|
1103
|
+
{
|
|
1104
|
+
d: "M0.308 1.2407C0.151 0.61 0.628 0 1.278 0H23.664C24.118 0 24.516 0.3065 24.631 0.746L30.671 23.746C30.837 24.38 30.359 25 29.704 25H6.982C6.523 25 6.122 24.6868 6.012 24.2407L0.308 1.2407Z",
|
|
1105
|
+
fill: "#FFBA00",
|
|
1106
|
+
opacity: isSelected ? 1 : 0.7
|
|
1107
|
+
}
|
|
1108
|
+
),
|
|
1109
|
+
/* @__PURE__ */ jsx("g", { transform: "translate(15, 12.5)", children: /* @__PURE__ */ jsxs("g", { transform: "rotate(-45)", children: [
|
|
1110
|
+
/* @__PURE__ */ jsx("rect", { x: "-6", y: "-3", width: "12", height: "6", rx: "1", fill: "none", stroke: "white", strokeWidth: "1.5" }),
|
|
1111
|
+
/* @__PURE__ */ jsx("line", { x1: "-2", y1: "-3", x2: "-2", y2: "3", stroke: "white", strokeWidth: "1.5" })
|
|
1112
|
+
] }) })
|
|
1113
|
+
] });
|
|
1114
|
+
var StarIcon = ({ isSelected }) => /* @__PURE__ */ jsxs("svg", { width: "42", height: "42", viewBox: "0 0 25 25", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
1115
|
+
/* @__PURE__ */ jsx(
|
|
1116
|
+
"path",
|
|
1117
|
+
{
|
|
1118
|
+
d: "M12.253 0.8403C12.65 0.393 13.35 0.393 13.747 0.8403L16.628 4.0796C16.805 4.2791 17.055 4.3993 17.321 4.4136L21.65 4.6461C22.248 4.6782 22.684 5.2247 22.582 5.8146L21.845 10.0863C21.8 10.3493 21.862 10.6196 22.017 10.8369L24.534 14.366C24.881 14.8533 24.726 15.5348 24.201 15.8231L20.402 17.9105C20.168 18.0391 19.995 18.2558 19.922 18.5125L18.732 22.6808C18.568 23.2564 17.938 23.5596 17.386 23.3292L13.385 21.6606C13.139 21.5578 12.861 21.5578 12.615 21.6606L8.614 23.3292C8.062 23.5596 7.432 23.2564 7.268 22.6808L6.078 18.5125C6.005 18.2558 5.832 18.0391 5.598 17.9105L1.799 15.8231C1.274 15.5348 1.119 14.8533 1.466 14.366L3.983 10.8369C4.138 10.6196 4.2 10.3493 4.155 10.0863L3.418 5.8146C3.316 5.2247 3.752 4.6782 4.35 4.6461L8.679 4.4136C8.945 4.3993 9.195 4.2791 9.372 4.0796L12.253 0.8403Z",
|
|
1119
|
+
fill: "#FF6800",
|
|
1120
|
+
opacity: isSelected ? 1 : 0.7
|
|
1121
|
+
}
|
|
1122
|
+
),
|
|
1123
|
+
/* @__PURE__ */ jsx("g", { transform: "translate(6.5, 6) scale(0.5)", children: /* @__PURE__ */ jsx(
|
|
1124
|
+
"path",
|
|
1125
|
+
{
|
|
1126
|
+
d: "M23.546 10.93L13.067 0.452c-0.604-0.603-1.582-0.603-2.188 0L8.708 2.627l2.76 2.76c0.645-0.215 1.379-0.07 1.889 0.441 0.516 0.516 0.658 1.258 0.438 1.9l2.658 2.66c0.645-0.223 1.387-0.078 1.9 0.435 0.721 0.72 0.721 1.884 0 2.604-0.719 0.719-1.881 0.719-2.6 0-0.539-0.541-0.674-1.337-0.404-1.996L12.86 8.955v6.525c0.176 0.086 0.342 0.203 0.488 0.348 0.713 0.721 0.713 1.883 0 2.6-0.719 0.721-1.889 0.721-2.609 0-0.719-0.719-0.719-1.879 0-2.598 0.182-0.18 0.387-0.316 0.605-0.406V8.835c-0.217-0.091-0.424-0.222-0.6-0.401-0.545-0.545-0.676-1.342-0.396-2.009L7.636 3.7 0.45 10.881c-0.6 0.605-0.6 1.584 0 2.189l10.48 10.477c0.604 0.604 1.582 0.604 2.186 0l10.43-10.43c0.605-0.603 0.605-1.582 0-2.187",
|
|
1127
|
+
fill: "white"
|
|
1128
|
+
}
|
|
1129
|
+
) })
|
|
1130
|
+
] });
|
|
1131
|
+
var ToolbarButton = ({ onClick, isSelected, icon, label }) => {
|
|
1132
|
+
const handleClick = (e) => {
|
|
1133
|
+
e.preventDefault();
|
|
1134
|
+
e.stopPropagation();
|
|
1135
|
+
onClick();
|
|
1136
|
+
};
|
|
1137
|
+
return /* @__PURE__ */ jsx(
|
|
1138
|
+
"button",
|
|
1139
|
+
{
|
|
1140
|
+
onClick: handleClick,
|
|
1141
|
+
title: label,
|
|
1142
|
+
type: "button",
|
|
1143
|
+
style: {
|
|
1144
|
+
display: "flex",
|
|
1145
|
+
alignItems: "center",
|
|
1146
|
+
justifyContent: "center",
|
|
1147
|
+
width: 56,
|
|
1148
|
+
height: 56,
|
|
1149
|
+
border: "none",
|
|
1150
|
+
borderRadius: 11,
|
|
1151
|
+
backgroundColor: isSelected ? "rgba(255,255,255,0.15)" : "transparent",
|
|
1152
|
+
cursor: "pointer",
|
|
1153
|
+
transition: "background-color 0.15s ease",
|
|
1154
|
+
pointerEvents: "auto"
|
|
1155
|
+
},
|
|
1156
|
+
children: icon
|
|
1157
|
+
}
|
|
1158
|
+
);
|
|
1159
|
+
};
|
|
1160
|
+
var SkemaToolbar = () => {
|
|
1161
|
+
const editor = useEditor();
|
|
1162
|
+
const tools = useTools();
|
|
1163
|
+
const isSelectSelected = useIsToolSelected(tools["select"]);
|
|
1164
|
+
const isDrawSelected = useIsToolSelected(tools["draw"]);
|
|
1165
|
+
const isLassoSelected = useIsToolSelected(tools["lasso-select"]);
|
|
1166
|
+
const isEraseSelected = useIsToolSelected(tools["eraser"]);
|
|
1167
|
+
const [isStarSelected] = useState(false);
|
|
1168
|
+
return /* @__PURE__ */ jsxs(
|
|
1169
|
+
"div",
|
|
1170
|
+
{
|
|
1171
|
+
"data-skema": "toolbar",
|
|
1172
|
+
style: {
|
|
1173
|
+
position: "absolute",
|
|
1174
|
+
bottom: 16,
|
|
1175
|
+
left: "50%",
|
|
1176
|
+
transform: "translateX(-50%)",
|
|
1177
|
+
display: "flex",
|
|
1178
|
+
alignItems: "center",
|
|
1179
|
+
gap: 11,
|
|
1180
|
+
padding: "11px 17px",
|
|
1181
|
+
backgroundColor: "white",
|
|
1182
|
+
borderRadius: 36,
|
|
1183
|
+
boxShadow: "0 2px 10px rgba(0,0,0,0.15)",
|
|
1184
|
+
pointerEvents: "auto",
|
|
1185
|
+
zIndex: 99999
|
|
1186
|
+
},
|
|
1187
|
+
children: [
|
|
1188
|
+
/* @__PURE__ */ jsx(
|
|
1189
|
+
ToolbarButton,
|
|
1190
|
+
{
|
|
1191
|
+
onClick: () => editor.setCurrentTool("select"),
|
|
1192
|
+
isSelected: isSelectSelected,
|
|
1193
|
+
icon: /* @__PURE__ */ jsx(SelectIcon, { isSelected: isSelectSelected }),
|
|
1194
|
+
label: "Select (V)"
|
|
1195
|
+
}
|
|
1196
|
+
),
|
|
1197
|
+
/* @__PURE__ */ jsx(
|
|
1198
|
+
ToolbarButton,
|
|
1199
|
+
{
|
|
1200
|
+
onClick: () => editor.setCurrentTool("lasso-select"),
|
|
1201
|
+
isSelected: isLassoSelected,
|
|
1202
|
+
icon: /* @__PURE__ */ jsx(LassoIcon, { isSelected: isLassoSelected }),
|
|
1203
|
+
label: "Lasso Select (L)"
|
|
1204
|
+
}
|
|
1205
|
+
),
|
|
1206
|
+
/* @__PURE__ */ jsx(
|
|
1207
|
+
ToolbarButton,
|
|
1208
|
+
{
|
|
1209
|
+
onClick: () => editor.setCurrentTool("draw"),
|
|
1210
|
+
isSelected: isDrawSelected,
|
|
1211
|
+
icon: /* @__PURE__ */ jsx(DrawIcon, { isSelected: isDrawSelected }),
|
|
1212
|
+
label: "Draw (D)"
|
|
1213
|
+
}
|
|
1214
|
+
),
|
|
1215
|
+
/* @__PURE__ */ jsx(
|
|
1216
|
+
ToolbarButton,
|
|
1217
|
+
{
|
|
1218
|
+
onClick: () => editor.setCurrentTool("eraser"),
|
|
1219
|
+
isSelected: isEraseSelected,
|
|
1220
|
+
icon: /* @__PURE__ */ jsx(EraseIcon, { isSelected: isEraseSelected }),
|
|
1221
|
+
label: "Eraser (E)"
|
|
1222
|
+
}
|
|
1223
|
+
),
|
|
1224
|
+
/* @__PURE__ */ jsx(
|
|
1225
|
+
"div",
|
|
1226
|
+
{
|
|
1227
|
+
style: {
|
|
1228
|
+
width: 3,
|
|
1229
|
+
height: 36,
|
|
1230
|
+
backgroundColor: "#C4C2C2",
|
|
1231
|
+
borderRadius: 1.5,
|
|
1232
|
+
margin: "0 6px"
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
),
|
|
1236
|
+
/* @__PURE__ */ jsx(
|
|
1237
|
+
ToolbarButton,
|
|
1238
|
+
{
|
|
1239
|
+
onClick: () => {
|
|
1240
|
+
console.log("Star button clicked - placeholder for future feature");
|
|
1241
|
+
},
|
|
1242
|
+
isSelected: isStarSelected,
|
|
1243
|
+
icon: /* @__PURE__ */ jsx(StarIcon, { isSelected: isStarSelected }),
|
|
1244
|
+
label: "Special (Coming Soon)"
|
|
1245
|
+
}
|
|
1246
|
+
)
|
|
1247
|
+
]
|
|
1248
|
+
}
|
|
1249
|
+
);
|
|
1250
|
+
};
|
|
1251
|
+
var LassoOverlay = () => {
|
|
1252
|
+
const editor = useEditor();
|
|
1253
|
+
const lassoPoints = useValue(
|
|
1254
|
+
"lasso points",
|
|
1255
|
+
() => {
|
|
1256
|
+
if (!editor.isIn("lasso-select.lassoing")) return [];
|
|
1257
|
+
const lassoing = editor.getStateDescendant("lasso-select.lassoing");
|
|
1258
|
+
return lassoing?.points?.get() ?? [];
|
|
1259
|
+
},
|
|
1260
|
+
[editor]
|
|
1261
|
+
);
|
|
1262
|
+
const svgPath = useMemo(() => {
|
|
1263
|
+
if (lassoPoints.length < 2) return "";
|
|
1264
|
+
let path = `M ${lassoPoints[0].x} ${lassoPoints[0].y}`;
|
|
1265
|
+
for (let i = 1; i < lassoPoints.length; i++) {
|
|
1266
|
+
path += ` L ${lassoPoints[i].x} ${lassoPoints[i].y}`;
|
|
1267
|
+
}
|
|
1268
|
+
path += " Z";
|
|
1269
|
+
return path;
|
|
1270
|
+
}, [lassoPoints]);
|
|
1271
|
+
if (lassoPoints.length === 0) return null;
|
|
1272
|
+
return /* @__PURE__ */ jsx("svg", { className: "tl-overlays__item", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
1273
|
+
"path",
|
|
1274
|
+
{
|
|
1275
|
+
d: svgPath,
|
|
1276
|
+
fill: "none",
|
|
1277
|
+
stroke: "rgba(59, 130, 246, 1)",
|
|
1278
|
+
strokeWidth: "calc(2px / var(--tl-zoom))",
|
|
1279
|
+
strokeLinecap: "round",
|
|
1280
|
+
strokeLinejoin: "round",
|
|
1281
|
+
strokeDasharray: "4 4"
|
|
1282
|
+
}
|
|
1283
|
+
) });
|
|
1284
|
+
};
|
|
1285
|
+
var SkemaOverlays = () => {
|
|
1286
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1287
|
+
/* @__PURE__ */ jsx(TldrawOverlays, {}),
|
|
1288
|
+
/* @__PURE__ */ jsx(LassoOverlay, {})
|
|
1289
|
+
] });
|
|
1290
|
+
};
|
|
1291
|
+
var SelectionOverlay = ({ selections }) => {
|
|
1292
|
+
const [scrollPos, setScrollPos] = useState({ x: 0, y: 0 });
|
|
1293
|
+
useEffect(() => {
|
|
1294
|
+
const handleScroll = () => {
|
|
1295
|
+
setScrollPos({ x: window.scrollX, y: window.scrollY });
|
|
1296
|
+
};
|
|
1297
|
+
handleScroll();
|
|
1298
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
1299
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
1300
|
+
}, []);
|
|
1301
|
+
return /* @__PURE__ */ jsx(Fragment, { children: selections.map((selection) => {
|
|
1302
|
+
const viewportX = selection.boundingBox.x - scrollPos.x;
|
|
1303
|
+
const viewportY = selection.boundingBox.y - scrollPos.y;
|
|
1304
|
+
return /* @__PURE__ */ jsx(
|
|
1305
|
+
"div",
|
|
1306
|
+
{
|
|
1307
|
+
"data-skema": "selection",
|
|
1308
|
+
style: {
|
|
1309
|
+
position: "fixed",
|
|
1310
|
+
left: viewportX,
|
|
1311
|
+
top: viewportY,
|
|
1312
|
+
width: selection.boundingBox.width,
|
|
1313
|
+
height: selection.boundingBox.height,
|
|
1314
|
+
border: "2px solid #10b981",
|
|
1315
|
+
backgroundColor: "rgba(16, 185, 129, 0.1)",
|
|
1316
|
+
pointerEvents: "none",
|
|
1317
|
+
zIndex: 999997
|
|
1318
|
+
},
|
|
1319
|
+
children: /* @__PURE__ */ jsx(
|
|
1320
|
+
"span",
|
|
1321
|
+
{
|
|
1322
|
+
style: {
|
|
1323
|
+
position: "absolute",
|
|
1324
|
+
top: -20,
|
|
1325
|
+
left: 0,
|
|
1326
|
+
backgroundColor: "#10b981",
|
|
1327
|
+
color: "white",
|
|
1328
|
+
padding: "2px 6px",
|
|
1329
|
+
fontSize: "11px",
|
|
1330
|
+
borderRadius: "3px",
|
|
1331
|
+
whiteSpace: "nowrap"
|
|
1332
|
+
},
|
|
1333
|
+
children: selection.tagName
|
|
1334
|
+
}
|
|
1335
|
+
)
|
|
1336
|
+
},
|
|
1337
|
+
selection.id
|
|
1338
|
+
);
|
|
1339
|
+
}) });
|
|
1340
|
+
};
|
|
1341
|
+
var AnnotationsSidebar = ({ annotations, onClear, onExport }) => {
|
|
1342
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1343
|
+
return /* @__PURE__ */ jsxs(
|
|
1344
|
+
"div",
|
|
1345
|
+
{
|
|
1346
|
+
"data-skema": "sidebar",
|
|
1347
|
+
style: {
|
|
1348
|
+
position: "fixed",
|
|
1349
|
+
right: isOpen ? 0 : -280,
|
|
1350
|
+
top: 60,
|
|
1351
|
+
width: 280,
|
|
1352
|
+
maxHeight: "calc(100vh - 120px)",
|
|
1353
|
+
backgroundColor: "white",
|
|
1354
|
+
borderRadius: "8px 0 0 8px",
|
|
1355
|
+
boxShadow: "-2px 0 10px rgba(0,0,0,0.1)",
|
|
1356
|
+
transition: "right 0.2s ease-out",
|
|
1357
|
+
zIndex: 999996,
|
|
1358
|
+
overflow: "hidden"
|
|
1359
|
+
},
|
|
1360
|
+
children: [
|
|
1361
|
+
/* @__PURE__ */ jsx(
|
|
1362
|
+
"button",
|
|
1363
|
+
{
|
|
1364
|
+
onClick: () => setIsOpen(!isOpen),
|
|
1365
|
+
style: {
|
|
1366
|
+
position: "absolute",
|
|
1367
|
+
left: -32,
|
|
1368
|
+
top: 10,
|
|
1369
|
+
width: 32,
|
|
1370
|
+
height: 32,
|
|
1371
|
+
backgroundColor: "#3b82f6",
|
|
1372
|
+
border: "none",
|
|
1373
|
+
borderRadius: "8px 0 0 8px",
|
|
1374
|
+
color: "white",
|
|
1375
|
+
cursor: "pointer",
|
|
1376
|
+
display: "flex",
|
|
1377
|
+
alignItems: "center",
|
|
1378
|
+
justifyContent: "center"
|
|
1379
|
+
},
|
|
1380
|
+
children: isOpen ? "\u2192" : "\u2190"
|
|
1381
|
+
}
|
|
1382
|
+
),
|
|
1383
|
+
/* @__PURE__ */ jsxs(
|
|
1384
|
+
"div",
|
|
1385
|
+
{
|
|
1386
|
+
style: {
|
|
1387
|
+
padding: "12px 16px",
|
|
1388
|
+
borderBottom: "1px solid #e5e7eb",
|
|
1389
|
+
display: "flex",
|
|
1390
|
+
justifyContent: "space-between",
|
|
1391
|
+
alignItems: "center"
|
|
1392
|
+
},
|
|
1393
|
+
children: [
|
|
1394
|
+
/* @__PURE__ */ jsxs("span", { style: { fontWeight: 600, fontSize: "14px" }, children: [
|
|
1395
|
+
"Annotations (",
|
|
1396
|
+
annotations.length,
|
|
1397
|
+
")"
|
|
1398
|
+
] }),
|
|
1399
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px" }, children: [
|
|
1400
|
+
/* @__PURE__ */ jsx(
|
|
1401
|
+
"button",
|
|
1402
|
+
{
|
|
1403
|
+
onClick: onExport,
|
|
1404
|
+
style: {
|
|
1405
|
+
padding: "4px 8px",
|
|
1406
|
+
fontSize: "12px",
|
|
1407
|
+
backgroundColor: "#3b82f6",
|
|
1408
|
+
color: "white",
|
|
1409
|
+
border: "none",
|
|
1410
|
+
borderRadius: "4px",
|
|
1411
|
+
cursor: "pointer"
|
|
1412
|
+
},
|
|
1413
|
+
children: "Export"
|
|
1414
|
+
}
|
|
1415
|
+
),
|
|
1416
|
+
/* @__PURE__ */ jsx(
|
|
1417
|
+
"button",
|
|
1418
|
+
{
|
|
1419
|
+
onClick: onClear,
|
|
1420
|
+
style: {
|
|
1421
|
+
padding: "4px 8px",
|
|
1422
|
+
fontSize: "12px",
|
|
1423
|
+
backgroundColor: "#ef4444",
|
|
1424
|
+
color: "white",
|
|
1425
|
+
border: "none",
|
|
1426
|
+
borderRadius: "4px",
|
|
1427
|
+
cursor: "pointer"
|
|
1428
|
+
},
|
|
1429
|
+
children: "Clear"
|
|
1430
|
+
}
|
|
1431
|
+
)
|
|
1432
|
+
] })
|
|
1433
|
+
]
|
|
1434
|
+
}
|
|
1435
|
+
),
|
|
1436
|
+
/* @__PURE__ */ jsx("div", { style: { overflowY: "auto", maxHeight: "calc(100vh - 200px)" }, children: annotations.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "16px", color: "#6b7280", fontSize: "13px" }, children: "No annotations yet. Use the DOM picker or drawing tools to annotate." }) : annotations.map((annotation) => /* @__PURE__ */ jsxs(
|
|
1437
|
+
"div",
|
|
1438
|
+
{
|
|
1439
|
+
style: {
|
|
1440
|
+
padding: "12px 16px",
|
|
1441
|
+
borderBottom: "1px solid #f3f4f6",
|
|
1442
|
+
fontSize: "13px"
|
|
1443
|
+
},
|
|
1444
|
+
children: [
|
|
1445
|
+
/* @__PURE__ */ jsxs("div", { style: { fontWeight: 500, marginBottom: "4px" }, children: [
|
|
1446
|
+
annotation.type === "dom_selection" && `\u{1F3AF} ${annotation.tagName}`,
|
|
1447
|
+
annotation.type === "drawing" && `\u270F\uFE0F ${annotation.comment || "Drawing"}`,
|
|
1448
|
+
annotation.type === "gesture" && `\u{1F446} ${annotation.gesture}`
|
|
1449
|
+
] }),
|
|
1450
|
+
annotation.type === "dom_selection" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1451
|
+
annotation.comment && /* @__PURE__ */ jsx("div", { style: { color: "#374151", fontSize: "12px", marginBottom: "4px" }, children: annotation.comment }),
|
|
1452
|
+
/* @__PURE__ */ jsx("div", { style: { color: "#6b7280", fontSize: "11px" }, children: annotation.selector.slice(0, 50) })
|
|
1453
|
+
] })
|
|
1454
|
+
]
|
|
1455
|
+
},
|
|
1456
|
+
annotation.id
|
|
1457
|
+
)) })
|
|
1458
|
+
]
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1461
|
+
};
|
|
1462
|
+
var Skema = ({
|
|
1463
|
+
enabled = true,
|
|
1464
|
+
onAnnotationsChange,
|
|
1465
|
+
onAnnotationSubmit,
|
|
1466
|
+
onAnnotationDelete,
|
|
1467
|
+
toggleShortcut = "mod+shift+e",
|
|
1468
|
+
initialAnnotations = [],
|
|
1469
|
+
zIndex = 99999
|
|
1470
|
+
}) => {
|
|
1471
|
+
const [isActive, setIsActive] = useState(enabled);
|
|
1472
|
+
const [annotations, setAnnotations] = useState(initialAnnotations);
|
|
1473
|
+
const [domSelections, setDomSelections] = useState([]);
|
|
1474
|
+
const [pendingAnnotation, setPendingAnnotation] = useState(null);
|
|
1475
|
+
const [pendingExiting, setPendingExiting] = useState(false);
|
|
1476
|
+
const [hoveredMarkerId, setHoveredMarkerId] = useState(null);
|
|
1477
|
+
const editorRef = useRef(null);
|
|
1478
|
+
const popupRef = useRef(null);
|
|
1479
|
+
const lastDoubleClickRef = useRef(0);
|
|
1480
|
+
useRef(false);
|
|
1481
|
+
const cleanupRef = useRef(null);
|
|
1482
|
+
useEffect(() => {
|
|
1483
|
+
const handleKeyDown = (e) => {
|
|
1484
|
+
const isMod = e.metaKey || e.ctrlKey;
|
|
1485
|
+
if (isMod && e.shiftKey && e.key.toLowerCase() === "e") {
|
|
1486
|
+
e.preventDefault();
|
|
1487
|
+
setIsActive((prev) => !prev);
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1491
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1492
|
+
}, []);
|
|
1493
|
+
const [scrollOffset, setScrollOffset] = useState({ x: 0, y: 0 });
|
|
1494
|
+
useEffect(() => {
|
|
1495
|
+
if (!isActive) return;
|
|
1496
|
+
const syncScroll = () => {
|
|
1497
|
+
const newOffset = { x: window.scrollX, y: window.scrollY };
|
|
1498
|
+
setScrollOffset(newOffset);
|
|
1499
|
+
if (editorRef.current) {
|
|
1500
|
+
editorRef.current.setCamera({ x: -newOffset.x, y: -newOffset.y, z: 1 });
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
syncScroll();
|
|
1504
|
+
window.addEventListener("scroll", syncScroll, { passive: true });
|
|
1505
|
+
return () => {
|
|
1506
|
+
window.removeEventListener("scroll", syncScroll);
|
|
1507
|
+
};
|
|
1508
|
+
}, [isActive]);
|
|
1509
|
+
useEffect(() => {
|
|
1510
|
+
if (!isActive) return;
|
|
1511
|
+
const handleWheel = (e) => {
|
|
1512
|
+
const target = e.target;
|
|
1513
|
+
if (target.closest(".tl-container") || target.closest('[data-skema="container"]')) {
|
|
1514
|
+
e.stopPropagation();
|
|
1515
|
+
window.scrollBy({
|
|
1516
|
+
top: e.deltaY,
|
|
1517
|
+
left: e.deltaX,
|
|
1518
|
+
behavior: "auto"
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
document.addEventListener("wheel", handleWheel, { capture: true, passive: false });
|
|
1523
|
+
return () => {
|
|
1524
|
+
document.removeEventListener("wheel", handleWheel, { capture: true });
|
|
1525
|
+
};
|
|
1526
|
+
}, [isActive]);
|
|
1527
|
+
useEffect(() => {
|
|
1528
|
+
onAnnotationsChange?.(annotations);
|
|
1529
|
+
}, [annotations, onAnnotationsChange]);
|
|
1530
|
+
const getSelectedDrawings = useCallback(() => {
|
|
1531
|
+
if (!editorRef.current) return [];
|
|
1532
|
+
const editor = editorRef.current;
|
|
1533
|
+
const selectedIds = editor.getSelectedShapeIds();
|
|
1534
|
+
const shapes = selectedIds.map((id) => editor.getShape(id)).filter(Boolean);
|
|
1535
|
+
return shapes.filter(
|
|
1536
|
+
(shape) => shape && ["draw", "line", "arrow", "geo", "text", "note"].includes(shape.type)
|
|
1537
|
+
);
|
|
1538
|
+
}, []);
|
|
1539
|
+
const handleDOMSelect = useCallback((selection) => {
|
|
1540
|
+
const selectedDrawings = getSelectedDrawings();
|
|
1541
|
+
const hasDrawings = selectedDrawings.length > 0;
|
|
1542
|
+
const rect = selection.boundingBox;
|
|
1543
|
+
const x = (rect.x + rect.width / 2) / window.innerWidth * 100;
|
|
1544
|
+
const clientY = rect.y - window.scrollY + rect.height / 2;
|
|
1545
|
+
let elementDesc = selection.tagName;
|
|
1546
|
+
if (hasDrawings) {
|
|
1547
|
+
elementDesc = `Drawing + ${selection.tagName}`;
|
|
1548
|
+
}
|
|
1549
|
+
setPendingAnnotation({
|
|
1550
|
+
x,
|
|
1551
|
+
y: rect.y + rect.height / 2,
|
|
1552
|
+
clientY,
|
|
1553
|
+
element: elementDesc,
|
|
1554
|
+
elementPath: selection.elementPath,
|
|
1555
|
+
selectedText: selection.text?.slice(0, 100),
|
|
1556
|
+
boundingBox: rect,
|
|
1557
|
+
isMultiSelect: hasDrawings,
|
|
1558
|
+
selections: [selection],
|
|
1559
|
+
annotationType: hasDrawings ? "drawing" : "dom_selection",
|
|
1560
|
+
shapeIds: hasDrawings ? editorRef.current?.getSelectedShapeIds() : void 0
|
|
1561
|
+
});
|
|
1562
|
+
}, [getSelectedDrawings]);
|
|
1563
|
+
const handleMultiDOMSelect = useCallback((selections) => {
|
|
1564
|
+
if (selections.length === 0) return;
|
|
1565
|
+
const selectedDrawings = getSelectedDrawings();
|
|
1566
|
+
const hasDrawings = selectedDrawings.length > 0;
|
|
1567
|
+
const minX = Math.min(...selections.map((s) => s.boundingBox.x));
|
|
1568
|
+
const minY = Math.min(...selections.map((s) => s.boundingBox.y));
|
|
1569
|
+
const maxX = Math.max(...selections.map((s) => s.boundingBox.x + s.boundingBox.width));
|
|
1570
|
+
const maxY = Math.max(...selections.map((s) => s.boundingBox.y + s.boundingBox.height));
|
|
1571
|
+
const combinedBounds = {
|
|
1572
|
+
x: minX,
|
|
1573
|
+
y: minY,
|
|
1574
|
+
width: maxX - minX,
|
|
1575
|
+
height: maxY - minY
|
|
1576
|
+
};
|
|
1577
|
+
const centerX = minX + (maxX - minX) / 2;
|
|
1578
|
+
const centerY = minY + (maxY - minY) / 2;
|
|
1579
|
+
const x = centerX / window.innerWidth * 100;
|
|
1580
|
+
const clientY = centerY - window.scrollY;
|
|
1581
|
+
const elementNames = selections.slice(0, 3).map((s) => s.tagName).join(", ");
|
|
1582
|
+
const suffix = selections.length > 3 ? ` +${selections.length - 3} more` : "";
|
|
1583
|
+
let element = `${selections.length} elements: ${elementNames}${suffix}`;
|
|
1584
|
+
if (hasDrawings) {
|
|
1585
|
+
const drawingCount = selectedDrawings.length;
|
|
1586
|
+
element = `Drawing (${drawingCount}) + ${element}`;
|
|
1587
|
+
}
|
|
1588
|
+
setPendingAnnotation({
|
|
1589
|
+
x,
|
|
1590
|
+
y: centerY,
|
|
1591
|
+
clientY,
|
|
1592
|
+
element,
|
|
1593
|
+
elementPath: "multi-select",
|
|
1594
|
+
boundingBox: combinedBounds,
|
|
1595
|
+
isMultiSelect: true,
|
|
1596
|
+
selections,
|
|
1597
|
+
annotationType: hasDrawings ? "drawing" : "dom_selection",
|
|
1598
|
+
shapeIds: hasDrawings ? editorRef.current?.getSelectedShapeIds() : void 0
|
|
1599
|
+
});
|
|
1600
|
+
}, [getSelectedDrawings]);
|
|
1601
|
+
const handleAnnotationSubmit = useCallback(async (comment) => {
|
|
1602
|
+
if (!pendingAnnotation) return;
|
|
1603
|
+
if (pendingAnnotation.annotationType === "dom_selection" && pendingAnnotation.selections) {
|
|
1604
|
+
const selections = pendingAnnotation.selections;
|
|
1605
|
+
if (selections.length === 1) {
|
|
1606
|
+
const selection = { ...selections[0], comment };
|
|
1607
|
+
setDomSelections((prev) => [...prev, selection]);
|
|
1608
|
+
setAnnotations((prev) => [...prev, { type: "dom_selection", ...selection }]);
|
|
1609
|
+
} else {
|
|
1610
|
+
const groupedSelection = {
|
|
1611
|
+
id: `group-${Date.now()}`,
|
|
1612
|
+
selector: selections.map((s) => s.selector).join(", "),
|
|
1613
|
+
tagName: pendingAnnotation.element,
|
|
1614
|
+
// Already formatted as "3 elements: div, span, p"
|
|
1615
|
+
elementPath: selections[0].elementPath,
|
|
1616
|
+
// Use first element's path as reference
|
|
1617
|
+
text: selections.map((s) => s.text).filter(Boolean).join(" | ").slice(0, 200),
|
|
1618
|
+
boundingBox: pendingAnnotation.boundingBox,
|
|
1619
|
+
timestamp: Date.now(),
|
|
1620
|
+
pathname: selections[0].pathname,
|
|
1621
|
+
comment,
|
|
1622
|
+
isMultiSelect: true,
|
|
1623
|
+
elements: selections.map((s) => ({
|
|
1624
|
+
selector: s.selector,
|
|
1625
|
+
tagName: s.tagName,
|
|
1626
|
+
elementPath: s.elementPath,
|
|
1627
|
+
text: s.text,
|
|
1628
|
+
boundingBox: s.boundingBox,
|
|
1629
|
+
cssClasses: s.cssClasses,
|
|
1630
|
+
attributes: s.attributes
|
|
1631
|
+
}))
|
|
1632
|
+
};
|
|
1633
|
+
setDomSelections((prev) => [...prev, groupedSelection]);
|
|
1634
|
+
setAnnotations((prev) => [...prev, { type: "dom_selection", ...groupedSelection }]);
|
|
1635
|
+
}
|
|
1636
|
+
} else if (pendingAnnotation.annotationType === "drawing" && pendingAnnotation.shapeIds) {
|
|
1637
|
+
const bbox = pendingAnnotation.boundingBox;
|
|
1638
|
+
const nearbyElements = pendingAnnotation.selections?.map((s) => ({
|
|
1639
|
+
selector: s.selector,
|
|
1640
|
+
tagName: s.tagName,
|
|
1641
|
+
text: s.text?.slice(0, 100)
|
|
1642
|
+
})) || [];
|
|
1643
|
+
const drawingAnnotation = {
|
|
1644
|
+
id: `drawing-${Date.now()}`,
|
|
1645
|
+
type: "drawing",
|
|
1646
|
+
tool: "draw",
|
|
1647
|
+
shapes: pendingAnnotation.shapeIds,
|
|
1648
|
+
boundingBox: bbox,
|
|
1649
|
+
timestamp: Date.now(),
|
|
1650
|
+
comment,
|
|
1651
|
+
nearbyElements
|
|
1652
|
+
};
|
|
1653
|
+
setAnnotations((prev) => [...prev, drawingAnnotation]);
|
|
1654
|
+
const drawingLog = `
|
|
1655
|
+
### ${annotations.length + 1}. Drawing (${pendingAnnotation.shapeIds?.length || 0} shapes)
|
|
1656
|
+
**Position:** x:${Math.round(bbox.x)}, y:${Math.round(bbox.y)} (${Math.round(bbox.width)}\xD7${Math.round(bbox.height)}px)
|
|
1657
|
+
**Annotation at:** ${((bbox.x + bbox.width / 2) / window.innerWidth * 100).toFixed(1)}% from left, ${Math.round(bbox.y + bbox.height / 2)}px from top
|
|
1658
|
+
**Shape IDs:** ${pendingAnnotation.shapeIds?.join(", ") || "none"}
|
|
1659
|
+
**Nearby Elements:** ${nearbyElements.map((e) => e.tagName).join(", ") || "none"}
|
|
1660
|
+
**Feedback:** ${comment}
|
|
1661
|
+
`;
|
|
1662
|
+
console.log(drawingLog);
|
|
1663
|
+
}
|
|
1664
|
+
if (pendingAnnotation.annotationType === "dom_selection" && pendingAnnotation.selections) {
|
|
1665
|
+
const selections = pendingAnnotation.selections;
|
|
1666
|
+
const annotationIndex = annotations.length + 1;
|
|
1667
|
+
const elementDescs = selections.map((s) => {
|
|
1668
|
+
const textPreview = s.text?.slice(0, 50) || "";
|
|
1669
|
+
return `${s.tagName.toLowerCase()}${textPreview ? `: "${textPreview}..."` : ""}`;
|
|
1670
|
+
}).join(", ");
|
|
1671
|
+
const firstSelection = selections[0];
|
|
1672
|
+
const firstElement = document.querySelector(firstSelection.selector);
|
|
1673
|
+
let computedStylesStr = "N/A";
|
|
1674
|
+
let nearbyElements = "N/A";
|
|
1675
|
+
if (firstElement) {
|
|
1676
|
+
const styles2 = window.getComputedStyle(firstElement);
|
|
1677
|
+
computedStylesStr = [
|
|
1678
|
+
`color: ${styles2.color}`,
|
|
1679
|
+
`border-color: ${styles2.borderColor}`,
|
|
1680
|
+
`font-size: ${styles2.fontSize}`,
|
|
1681
|
+
`font-weight: ${styles2.fontWeight}`,
|
|
1682
|
+
`font-family: ${styles2.fontFamily}`,
|
|
1683
|
+
`line-height: ${styles2.lineHeight}`,
|
|
1684
|
+
`letter-spacing: ${styles2.letterSpacing}`,
|
|
1685
|
+
`text-align: ${styles2.textAlign}`,
|
|
1686
|
+
`width: ${styles2.width}`,
|
|
1687
|
+
`height: ${styles2.height}`,
|
|
1688
|
+
`border: ${styles2.border}`,
|
|
1689
|
+
`display: ${styles2.display}`,
|
|
1690
|
+
`flex-direction: ${styles2.flexDirection}`,
|
|
1691
|
+
`opacity: ${styles2.opacity}`
|
|
1692
|
+
].join("; ");
|
|
1693
|
+
const parent = firstElement.parentElement;
|
|
1694
|
+
if (parent) {
|
|
1695
|
+
const siblings = Array.from(parent.children).filter((el) => el !== firstElement).slice(0, 3).map((el) => el.tagName.toLowerCase());
|
|
1696
|
+
nearbyElements = siblings.length > 0 ? siblings.join(", ") : "none";
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const bbox = pendingAnnotation.boundingBox;
|
|
1700
|
+
const forensicLog = `
|
|
1701
|
+
### ${annotationIndex}. ${selections.length > 1 ? `${selections.length} elements: ` : ""}${elementDescs}
|
|
1702
|
+
${selections.length > 1 ? "*Forensic data shown for first element of selection*\n" : ""}**Full DOM Path:** ${firstSelection.elementPath}
|
|
1703
|
+
**Position:** x:${Math.round(bbox.x)}, y:${Math.round(bbox.y)} (${Math.round(bbox.width)}\xD7${Math.round(bbox.height)}px)
|
|
1704
|
+
**Annotation at:** ${((bbox.x + bbox.width / 2) / window.innerWidth * 100).toFixed(1)}% from left, ${Math.round(bbox.y + bbox.height / 2)}px from top
|
|
1705
|
+
**Computed Styles:** ${computedStylesStr}
|
|
1706
|
+
**Nearby Elements:** ${nearbyElements}
|
|
1707
|
+
**Feedback:** ${comment}
|
|
1708
|
+
`;
|
|
1709
|
+
console.log(forensicLog);
|
|
1710
|
+
}
|
|
1711
|
+
if (onAnnotationSubmit) {
|
|
1712
|
+
let submittedAnnotation;
|
|
1713
|
+
if (pendingAnnotation.annotationType === "dom_selection" && pendingAnnotation.selections) {
|
|
1714
|
+
const selections = pendingAnnotation.selections;
|
|
1715
|
+
if (selections.length === 1) {
|
|
1716
|
+
submittedAnnotation = { type: "dom_selection", ...selections[0], comment };
|
|
1717
|
+
} else {
|
|
1718
|
+
submittedAnnotation = {
|
|
1719
|
+
type: "dom_selection",
|
|
1720
|
+
id: `group-${Date.now()}`,
|
|
1721
|
+
selector: selections.map((s) => s.selector).join(", "),
|
|
1722
|
+
tagName: pendingAnnotation.element,
|
|
1723
|
+
elementPath: selections[0].elementPath,
|
|
1724
|
+
text: selections.map((s) => s.text).filter(Boolean).join(" | ").slice(0, 200),
|
|
1725
|
+
boundingBox: pendingAnnotation.boundingBox,
|
|
1726
|
+
timestamp: Date.now(),
|
|
1727
|
+
pathname: selections[0].pathname,
|
|
1728
|
+
comment,
|
|
1729
|
+
isMultiSelect: true,
|
|
1730
|
+
elements: selections.map((s) => ({
|
|
1731
|
+
selector: s.selector,
|
|
1732
|
+
tagName: s.tagName,
|
|
1733
|
+
elementPath: s.elementPath,
|
|
1734
|
+
text: s.text,
|
|
1735
|
+
boundingBox: s.boundingBox,
|
|
1736
|
+
cssClasses: s.cssClasses,
|
|
1737
|
+
attributes: s.attributes
|
|
1738
|
+
}))
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
} else {
|
|
1742
|
+
const editor = editorRef.current;
|
|
1743
|
+
let drawingSvg;
|
|
1744
|
+
let drawingImage;
|
|
1745
|
+
let extractedText;
|
|
1746
|
+
const gridConfig = { color: "#0066FF", size: 100, labels: true };
|
|
1747
|
+
console.log("[Skema] Drawing annotation - shapeIds:", pendingAnnotation.shapeIds);
|
|
1748
|
+
if (editor && pendingAnnotation.shapeIds && pendingAnnotation.shapeIds.length > 0) {
|
|
1749
|
+
const shapeIds = pendingAnnotation.shapeIds;
|
|
1750
|
+
console.log("[Skema] Exporting", shapeIds.length, "shapes for image...");
|
|
1751
|
+
try {
|
|
1752
|
+
const svgResult = await editor.getSvgString(shapeIds, {
|
|
1753
|
+
padding: 20,
|
|
1754
|
+
background: false
|
|
1755
|
+
});
|
|
1756
|
+
if (svgResult?.svg) {
|
|
1757
|
+
drawingSvg = addGridToSvg(svgResult.svg, gridConfig);
|
|
1758
|
+
console.log("[Skema] SVG export successful");
|
|
1759
|
+
}
|
|
1760
|
+
} catch (e) {
|
|
1761
|
+
console.warn("[Skema] Failed to export drawing SVG:", e);
|
|
1762
|
+
}
|
|
1763
|
+
try {
|
|
1764
|
+
const imageResult = await editor.toImage(shapeIds, {
|
|
1765
|
+
format: "png",
|
|
1766
|
+
padding: 20,
|
|
1767
|
+
background: true
|
|
1768
|
+
});
|
|
1769
|
+
console.log("[Skema] toImage result:", imageResult ? "got result" : "null", imageResult?.blob ? "has blob" : "no blob");
|
|
1770
|
+
if (imageResult?.blob) {
|
|
1771
|
+
drawingImage = await blobToBase64(imageResult.blob);
|
|
1772
|
+
console.log("[Skema] Image export successful, base64 length:", drawingImage?.length);
|
|
1773
|
+
}
|
|
1774
|
+
} catch (e) {
|
|
1775
|
+
console.warn("[Skema] Failed to export drawing image:", e);
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
const shapes = shapeIds.map((id) => editor.getShape(id)).filter(Boolean);
|
|
1779
|
+
extractedText = extractTextFromShapes(shapes);
|
|
1780
|
+
} catch (e) {
|
|
1781
|
+
console.warn("[Skema] Failed to extract text from shapes:", e);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const nearbyElements = pendingAnnotation.boundingBox ? findNearbyElementsWithStyles(pendingAnnotation.boundingBox, 5) : [];
|
|
1785
|
+
const projectStyles = extractProjectStyleContext();
|
|
1786
|
+
const viewport = getViewportInfo();
|
|
1787
|
+
submittedAnnotation = {
|
|
1788
|
+
id: `drawing-${Date.now()}`,
|
|
1789
|
+
type: "drawing",
|
|
1790
|
+
tool: "draw",
|
|
1791
|
+
shapes: pendingAnnotation.shapeIds || [],
|
|
1792
|
+
boundingBox: pendingAnnotation.boundingBox,
|
|
1793
|
+
timestamp: Date.now(),
|
|
1794
|
+
comment,
|
|
1795
|
+
drawingSvg,
|
|
1796
|
+
drawingImage,
|
|
1797
|
+
extractedText: extractedText || void 0,
|
|
1798
|
+
gridConfig,
|
|
1799
|
+
nearbyElements,
|
|
1800
|
+
viewport,
|
|
1801
|
+
projectStyles
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
onAnnotationSubmit(submittedAnnotation, comment);
|
|
1805
|
+
}
|
|
1806
|
+
setPendingExiting(true);
|
|
1807
|
+
setTimeout(() => {
|
|
1808
|
+
setPendingAnnotation(null);
|
|
1809
|
+
setPendingExiting(false);
|
|
1810
|
+
}, 150);
|
|
1811
|
+
}, [pendingAnnotation, onAnnotationSubmit]);
|
|
1812
|
+
const handleAnnotationCancel = useCallback(() => {
|
|
1813
|
+
setPendingExiting(true);
|
|
1814
|
+
setTimeout(() => {
|
|
1815
|
+
setPendingAnnotation(null);
|
|
1816
|
+
setPendingExiting(false);
|
|
1817
|
+
}, 150);
|
|
1818
|
+
}, []);
|
|
1819
|
+
const handleDeleteAnnotation = useCallback((annotation) => {
|
|
1820
|
+
setAnnotations((prev) => prev.filter((a) => a.id !== annotation.id));
|
|
1821
|
+
if (annotation.type === "dom_selection") {
|
|
1822
|
+
setDomSelections((prev) => prev.filter((s) => s.id !== annotation.id));
|
|
1823
|
+
}
|
|
1824
|
+
setHoveredMarkerId(null);
|
|
1825
|
+
onAnnotationDelete?.(annotation.id);
|
|
1826
|
+
}, [onAnnotationDelete]);
|
|
1827
|
+
const handleClear = useCallback(() => {
|
|
1828
|
+
setAnnotations([]);
|
|
1829
|
+
setDomSelections([]);
|
|
1830
|
+
if (editorRef.current) {
|
|
1831
|
+
editorRef.current.selectAll();
|
|
1832
|
+
editorRef.current.deleteShapes(editorRef.current.getSelectedShapeIds());
|
|
1833
|
+
}
|
|
1834
|
+
}, []);
|
|
1835
|
+
const handleExport = useCallback(() => {
|
|
1836
|
+
const exportData = {
|
|
1837
|
+
version: "1.0.0",
|
|
1838
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1839
|
+
viewport: getViewportInfo(),
|
|
1840
|
+
pathname: window.location.pathname,
|
|
1841
|
+
annotations
|
|
1842
|
+
};
|
|
1843
|
+
navigator.clipboard.writeText(JSON.stringify(exportData, null, 2));
|
|
1844
|
+
console.log("[Skema] Exported annotations:", exportData);
|
|
1845
|
+
alert("Annotations copied to clipboard!");
|
|
1846
|
+
}, [annotations]);
|
|
1847
|
+
const findDOMElementsInBounds = useCallback((bounds) => {
|
|
1848
|
+
const elements = [];
|
|
1849
|
+
const allElements = document.querySelectorAll("*");
|
|
1850
|
+
allElements.forEach((el) => {
|
|
1851
|
+
if (!(el instanceof HTMLElement)) return;
|
|
1852
|
+
if (shouldIgnoreElement(el)) return;
|
|
1853
|
+
const rect = el.getBoundingClientRect();
|
|
1854
|
+
const elBounds = {
|
|
1855
|
+
x: rect.left,
|
|
1856
|
+
y: rect.top,
|
|
1857
|
+
width: rect.width,
|
|
1858
|
+
height: rect.height
|
|
1859
|
+
};
|
|
1860
|
+
if (elBounds.width < 10 || elBounds.height < 10) return;
|
|
1861
|
+
if (bboxIntersects(bounds, elBounds)) {
|
|
1862
|
+
const isParent = elements.some((existing) => el.contains(existing));
|
|
1863
|
+
if (!isParent) {
|
|
1864
|
+
const filtered = elements.filter((existing) => !existing.contains(el) && !el.contains(existing));
|
|
1865
|
+
filtered.push(el);
|
|
1866
|
+
elements.length = 0;
|
|
1867
|
+
elements.push(...filtered);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
return elements;
|
|
1872
|
+
}, []);
|
|
1873
|
+
const handleDrawingAnnotation = useCallback((selectedIds) => {
|
|
1874
|
+
if (!editorRef.current || selectedIds.length === 0) return;
|
|
1875
|
+
if (pendingAnnotation) return;
|
|
1876
|
+
const editor = editorRef.current;
|
|
1877
|
+
const selectedShapes = selectedIds.map((id) => editor.getShape(id)).filter(Boolean);
|
|
1878
|
+
if (selectedShapes.length === 0) return;
|
|
1879
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1880
|
+
for (const shape of selectedShapes) {
|
|
1881
|
+
if (!shape) continue;
|
|
1882
|
+
const bounds = editor.getShapePageBounds(shape.id);
|
|
1883
|
+
if (bounds) {
|
|
1884
|
+
minX = Math.min(minX, bounds.x);
|
|
1885
|
+
minY = Math.min(minY, bounds.y);
|
|
1886
|
+
maxX = Math.max(maxX, bounds.x + bounds.width);
|
|
1887
|
+
maxY = Math.max(maxY, bounds.y + bounds.height);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (minX === Infinity) return;
|
|
1891
|
+
const selectionBounds = {
|
|
1892
|
+
x: minX,
|
|
1893
|
+
y: minY,
|
|
1894
|
+
width: maxX - minX,
|
|
1895
|
+
height: maxY - minY
|
|
1896
|
+
};
|
|
1897
|
+
const drawingShapes = selectedShapes.filter(
|
|
1898
|
+
(shape) => shape && ["draw", "line", "arrow", "geo", "text", "note"].includes(shape.type)
|
|
1899
|
+
);
|
|
1900
|
+
if (drawingShapes.length === 0) return;
|
|
1901
|
+
const viewportBounds = {
|
|
1902
|
+
x: selectionBounds.x - window.scrollX,
|
|
1903
|
+
y: selectionBounds.y - window.scrollY,
|
|
1904
|
+
width: selectionBounds.width,
|
|
1905
|
+
height: selectionBounds.height
|
|
1906
|
+
};
|
|
1907
|
+
const domElements = findDOMElementsInBounds(viewportBounds);
|
|
1908
|
+
const centerX = selectionBounds.x + selectionBounds.width / 2;
|
|
1909
|
+
const centerY = selectionBounds.y + selectionBounds.height / 2;
|
|
1910
|
+
const x = (centerX - window.scrollX) / window.innerWidth * 100;
|
|
1911
|
+
const clientY = centerY - window.scrollY;
|
|
1912
|
+
let elementDesc = `Drawing (${drawingShapes.length} shape${drawingShapes.length > 1 ? "s" : ""})`;
|
|
1913
|
+
if (domElements.length > 0) {
|
|
1914
|
+
const domNames = domElements.slice(0, 2).map((el) => el.tagName.toLowerCase()).join(", ");
|
|
1915
|
+
const domSuffix = domElements.length > 2 ? ` +${domElements.length - 2} more` : "";
|
|
1916
|
+
elementDesc += ` + ${domNames}${domSuffix}`;
|
|
1917
|
+
}
|
|
1918
|
+
const newDomSelections = domElements.map((el) => createDOMSelection(el));
|
|
1919
|
+
setPendingAnnotation({
|
|
1920
|
+
x,
|
|
1921
|
+
y: centerY,
|
|
1922
|
+
clientY,
|
|
1923
|
+
element: elementDesc,
|
|
1924
|
+
elementPath: "drawing",
|
|
1925
|
+
boundingBox: {
|
|
1926
|
+
x: selectionBounds.x,
|
|
1927
|
+
y: selectionBounds.y,
|
|
1928
|
+
width: selectionBounds.width,
|
|
1929
|
+
height: selectionBounds.height
|
|
1930
|
+
},
|
|
1931
|
+
isMultiSelect: drawingShapes.length > 1 || domElements.length > 0,
|
|
1932
|
+
annotationType: "drawing",
|
|
1933
|
+
shapeIds: selectedIds,
|
|
1934
|
+
selections: newDomSelections.length > 0 ? newDomSelections : void 0
|
|
1935
|
+
});
|
|
1936
|
+
}, [pendingAnnotation, findDOMElementsInBounds]);
|
|
1937
|
+
const handleBrushSelection = useCallback((brushBounds) => {
|
|
1938
|
+
const viewportBounds = {
|
|
1939
|
+
x: brushBounds.x - window.scrollX,
|
|
1940
|
+
y: brushBounds.y - window.scrollY,
|
|
1941
|
+
width: brushBounds.width,
|
|
1942
|
+
height: brushBounds.height
|
|
1943
|
+
};
|
|
1944
|
+
const foundElements = findDOMElementsInBounds(viewportBounds);
|
|
1945
|
+
const newElements = foundElements.filter((el) => {
|
|
1946
|
+
const rect = el.getBoundingClientRect();
|
|
1947
|
+
return !domSelections.some(
|
|
1948
|
+
(s) => Math.abs(s.boundingBox.x - (rect.left + window.scrollX)) < 5 && Math.abs(s.boundingBox.y - (rect.top + window.scrollY)) < 5
|
|
1949
|
+
);
|
|
1950
|
+
});
|
|
1951
|
+
if (newElements.length === 0) return;
|
|
1952
|
+
const selections = newElements.map((el) => createDOMSelection(el));
|
|
1953
|
+
if (selections.length === 1) {
|
|
1954
|
+
handleDOMSelect(selections[0]);
|
|
1955
|
+
} else {
|
|
1956
|
+
handleMultiDOMSelect(selections);
|
|
1957
|
+
}
|
|
1958
|
+
}, [findDOMElementsInBounds, domSelections, handleDOMSelect, handleMultiDOMSelect]);
|
|
1959
|
+
const isPointInPolygon = useCallback((point, polygon) => {
|
|
1960
|
+
let inside = false;
|
|
1961
|
+
const n = polygon.length;
|
|
1962
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
1963
|
+
const xi = polygon[i].x;
|
|
1964
|
+
const yi = polygon[i].y;
|
|
1965
|
+
const xj = polygon[j].x;
|
|
1966
|
+
const yj = polygon[j].y;
|
|
1967
|
+
const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
|
|
1968
|
+
if (intersect) {
|
|
1969
|
+
inside = !inside;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return inside;
|
|
1973
|
+
}, []);
|
|
1974
|
+
const handleLassoSelection = useCallback((lassoPoints) => {
|
|
1975
|
+
if (lassoPoints.length < 3) return;
|
|
1976
|
+
const viewportPoints = lassoPoints.map((p) => ({
|
|
1977
|
+
x: p.x - window.scrollX,
|
|
1978
|
+
y: p.y - window.scrollY
|
|
1979
|
+
}));
|
|
1980
|
+
let lassoMinX = Infinity, lassoMinY = Infinity;
|
|
1981
|
+
let lassoMaxX = -Infinity, lassoMaxY = -Infinity;
|
|
1982
|
+
for (const p of viewportPoints) {
|
|
1983
|
+
lassoMinX = Math.min(lassoMinX, p.x);
|
|
1984
|
+
lassoMinY = Math.min(lassoMinY, p.y);
|
|
1985
|
+
lassoMaxX = Math.max(lassoMaxX, p.x);
|
|
1986
|
+
lassoMaxY = Math.max(lassoMaxY, p.y);
|
|
1987
|
+
}
|
|
1988
|
+
const allElements = document.querySelectorAll("*");
|
|
1989
|
+
const foundElements = [];
|
|
1990
|
+
allElements.forEach((el) => {
|
|
1991
|
+
if (!(el instanceof HTMLElement)) return;
|
|
1992
|
+
if (shouldIgnoreElement(el)) return;
|
|
1993
|
+
const rect = el.getBoundingClientRect();
|
|
1994
|
+
if (rect.width < 10 || rect.height < 10) return;
|
|
1995
|
+
const boundsOverlap = !(rect.left > lassoMaxX || rect.right < lassoMinX || rect.top > lassoMaxY || rect.bottom < lassoMinY);
|
|
1996
|
+
if (boundsOverlap) {
|
|
1997
|
+
const isParent = foundElements.some((existing) => el.contains(existing));
|
|
1998
|
+
if (!isParent) {
|
|
1999
|
+
const filtered = foundElements.filter((existing) => !existing.contains(el) && !el.contains(existing));
|
|
2000
|
+
filtered.push(el);
|
|
2001
|
+
foundElements.length = 0;
|
|
2002
|
+
foundElements.push(...filtered);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
const newElements = foundElements.filter((el) => {
|
|
2007
|
+
const rect = el.getBoundingClientRect();
|
|
2008
|
+
return !domSelections.some(
|
|
2009
|
+
(s) => Math.abs(s.boundingBox.x - (rect.left + window.scrollX)) < 5 && Math.abs(s.boundingBox.y - (rect.top + window.scrollY)) < 5
|
|
2010
|
+
);
|
|
2011
|
+
});
|
|
2012
|
+
if (newElements.length === 0) return;
|
|
2013
|
+
const selections = newElements.map((el) => createDOMSelection(el));
|
|
2014
|
+
if (selections.length === 1) {
|
|
2015
|
+
handleDOMSelect(selections[0]);
|
|
2016
|
+
} else {
|
|
2017
|
+
handleMultiDOMSelect(selections);
|
|
2018
|
+
}
|
|
2019
|
+
}, [isPointInPolygon, domSelections, handleDOMSelect, handleMultiDOMSelect]);
|
|
2020
|
+
const handleMount = useCallback((editor) => {
|
|
2021
|
+
editorRef.current = editor;
|
|
2022
|
+
editor.setStyleForNextShapes(ArrowShapeKindStyle, "arc");
|
|
2023
|
+
try {
|
|
2024
|
+
const selectIdleState = editor.getStateDescendant("select.idle");
|
|
2025
|
+
if (selectIdleState) {
|
|
2026
|
+
selectIdleState.handleDoubleClickOnCanvas = (info) => {
|
|
2027
|
+
lastDoubleClickRef.current = Date.now();
|
|
2028
|
+
const point = editor.pageToViewport(info.point);
|
|
2029
|
+
const elements = document.elementsFromPoint(point.x, point.y);
|
|
2030
|
+
const target = elements.find(
|
|
2031
|
+
(el) => el instanceof HTMLElement && !shouldIgnoreElement(el)
|
|
2032
|
+
);
|
|
2033
|
+
if (target) {
|
|
2034
|
+
const selection = createDOMSelection(target);
|
|
2035
|
+
const rect = selection.boundingBox;
|
|
2036
|
+
const x = (rect.x + rect.width / 2) / window.innerWidth * 100;
|
|
2037
|
+
const clientY = rect.y - window.scrollY + rect.height / 2;
|
|
2038
|
+
setPendingAnnotation({
|
|
2039
|
+
x,
|
|
2040
|
+
y: rect.y + rect.height / 2,
|
|
2041
|
+
clientY,
|
|
2042
|
+
element: selection.tagName,
|
|
2043
|
+
elementPath: selection.elementPath,
|
|
2044
|
+
selectedText: selection.text?.slice(0, 100),
|
|
2045
|
+
boundingBox: rect,
|
|
2046
|
+
isMultiSelect: false,
|
|
2047
|
+
selections: [selection],
|
|
2048
|
+
annotationType: "dom_selection"
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
} catch (e) {
|
|
2054
|
+
console.warn("Failed to override double click behavior", e);
|
|
2055
|
+
}
|
|
2056
|
+
editor.setCamera({ x: -window.scrollX, y: -window.scrollY, z: 1 });
|
|
2057
|
+
editor.sideEffects.registerAfterChangeHandler("camera", () => {
|
|
2058
|
+
const camera = editor.getCamera();
|
|
2059
|
+
if (camera.z !== 1) {
|
|
2060
|
+
editor.setCamera({ x: camera.x, y: camera.y, z: 1 });
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
const lassoSelectTool = editor.root.children?.["lasso-select"];
|
|
2064
|
+
if (lassoSelectTool) {
|
|
2065
|
+
if ("setOnLassoComplete" in lassoSelectTool) {
|
|
2066
|
+
lassoSelectTool.setOnLassoComplete((points) => {
|
|
2067
|
+
handleLassoSelection(points);
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
let lastBrush = null;
|
|
2072
|
+
editor.sideEffects.registerAfterChangeHandler("instance", (prev, next) => {
|
|
2073
|
+
if (prev.brush && !next.brush && lastBrush) {
|
|
2074
|
+
const brushBounds = {
|
|
2075
|
+
x: lastBrush.x,
|
|
2076
|
+
y: lastBrush.y,
|
|
2077
|
+
width: lastBrush.w,
|
|
2078
|
+
height: lastBrush.h
|
|
2079
|
+
};
|
|
2080
|
+
handleBrushSelection(brushBounds);
|
|
2081
|
+
lastBrush = null;
|
|
2082
|
+
} else if (next.brush) {
|
|
2083
|
+
lastBrush = next.brush;
|
|
2084
|
+
}
|
|
2085
|
+
return;
|
|
2086
|
+
});
|
|
2087
|
+
}, [handleDOMSelect, handleBrushSelection, handleLassoSelection, handleMultiDOMSelect, handleDrawingAnnotation]);
|
|
2088
|
+
useEffect(() => {
|
|
2089
|
+
return () => {
|
|
2090
|
+
if (cleanupRef.current) {
|
|
2091
|
+
cleanupRef.current();
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
}, []);
|
|
2095
|
+
useEffect(() => {
|
|
2096
|
+
if (!isActive) return;
|
|
2097
|
+
const handlePointerDown = (e) => {
|
|
2098
|
+
if (e.button !== 0) return;
|
|
2099
|
+
const target = e.target;
|
|
2100
|
+
if (target.closest('[data-skema="annotation-popup"]')) return;
|
|
2101
|
+
if (pendingAnnotation) {
|
|
2102
|
+
e.preventDefault();
|
|
2103
|
+
e.stopPropagation();
|
|
2104
|
+
handleAnnotationCancel();
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
if (e.shiftKey) return;
|
|
2108
|
+
if (!target.closest(".tl-canvas")) return;
|
|
2109
|
+
};
|
|
2110
|
+
document.addEventListener("pointerdown", handlePointerDown, { capture: true });
|
|
2111
|
+
return () => document.removeEventListener("pointerdown", handlePointerDown, { capture: true });
|
|
2112
|
+
}, [isActive, pendingAnnotation, handleAnnotationCancel]);
|
|
2113
|
+
const components = {
|
|
2114
|
+
Toolbar: null,
|
|
2115
|
+
Overlays: SkemaOverlays,
|
|
2116
|
+
// Hide background to make canvas transparent (so website shows through)
|
|
2117
|
+
Background: null,
|
|
2118
|
+
// Hide UI elements we don't need
|
|
2119
|
+
SharePanel: null,
|
|
2120
|
+
MenuPanel: null,
|
|
2121
|
+
TopPanel: null,
|
|
2122
|
+
PageMenu: null,
|
|
2123
|
+
NavigationPanel: null,
|
|
2124
|
+
HelpMenu: null,
|
|
2125
|
+
Minimap: null,
|
|
2126
|
+
// Hide "Back to Content" button (HelperButtons contains this)
|
|
2127
|
+
HelperButtons: null,
|
|
2128
|
+
QuickActions: null,
|
|
2129
|
+
ZoomMenu: null,
|
|
2130
|
+
ActionsMenu: null,
|
|
2131
|
+
DebugPanel: null,
|
|
2132
|
+
DebugMenu: null,
|
|
2133
|
+
// Hide canvas overlays
|
|
2134
|
+
OnTheCanvas: null,
|
|
2135
|
+
InFrontOfTheCanvas: null
|
|
2136
|
+
};
|
|
2137
|
+
const overrides = {
|
|
2138
|
+
tools(editor, tools) {
|
|
2139
|
+
return {
|
|
2140
|
+
...tools,
|
|
2141
|
+
"lasso-select": {
|
|
2142
|
+
id: "lasso-select",
|
|
2143
|
+
label: "Lasso Select",
|
|
2144
|
+
icon: "blob",
|
|
2145
|
+
kbd: "l",
|
|
2146
|
+
onSelect: () => {
|
|
2147
|
+
editor.setCurrentTool("lasso-select");
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
if (!isActive) {
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
return /* @__PURE__ */ jsxs(
|
|
2157
|
+
"div",
|
|
2158
|
+
{
|
|
2159
|
+
"data-skema": "container",
|
|
2160
|
+
style: {
|
|
2161
|
+
position: "fixed",
|
|
2162
|
+
inset: 0,
|
|
2163
|
+
zIndex,
|
|
2164
|
+
pointerEvents: "none"
|
|
2165
|
+
},
|
|
2166
|
+
children: [
|
|
2167
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
2168
|
+
.tlui-button[data-testid="back-to-content"],
|
|
2169
|
+
.tlui-offscreen-indicator,
|
|
2170
|
+
[class*="back-to-content"],
|
|
2171
|
+
.tl-offscreen-indicator {
|
|
2172
|
+
display: none !important;
|
|
2173
|
+
}
|
|
2174
|
+
` }),
|
|
2175
|
+
/* @__PURE__ */ jsx(
|
|
2176
|
+
"div",
|
|
2177
|
+
{
|
|
2178
|
+
style: {
|
|
2179
|
+
position: "absolute",
|
|
2180
|
+
inset: 0,
|
|
2181
|
+
pointerEvents: "auto"
|
|
2182
|
+
},
|
|
2183
|
+
children: /* @__PURE__ */ jsx(
|
|
2184
|
+
Tldraw,
|
|
2185
|
+
{
|
|
2186
|
+
tools: [LassoSelectTool],
|
|
2187
|
+
components,
|
|
2188
|
+
overrides,
|
|
2189
|
+
onMount: handleMount,
|
|
2190
|
+
hideUi: false,
|
|
2191
|
+
inferDarkMode: false,
|
|
2192
|
+
options: {
|
|
2193
|
+
// Disable camera constraints that would interfere with overlay mode
|
|
2194
|
+
maxPages: 1
|
|
2195
|
+
},
|
|
2196
|
+
children: /* @__PURE__ */ jsx(SkemaToolbar, {})
|
|
2197
|
+
}
|
|
2198
|
+
)
|
|
2199
|
+
}
|
|
2200
|
+
),
|
|
2201
|
+
/* @__PURE__ */ jsx(SelectionOverlay, { selections: domSelections }),
|
|
2202
|
+
/* @__PURE__ */ jsx(
|
|
2203
|
+
AnnotationMarkersLayer,
|
|
2204
|
+
{
|
|
2205
|
+
annotations,
|
|
2206
|
+
scrollOffset,
|
|
2207
|
+
hoveredMarkerId,
|
|
2208
|
+
onHover: setHoveredMarkerId,
|
|
2209
|
+
onDelete: handleDeleteAnnotation
|
|
2210
|
+
}
|
|
2211
|
+
),
|
|
2212
|
+
pendingAnnotation && pendingAnnotation.boundingBox && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2213
|
+
/* @__PURE__ */ jsx(
|
|
2214
|
+
"div",
|
|
2215
|
+
{
|
|
2216
|
+
"data-skema": "pending-highlight",
|
|
2217
|
+
style: {
|
|
2218
|
+
position: "fixed",
|
|
2219
|
+
left: pendingAnnotation.boundingBox.x - scrollOffset.x,
|
|
2220
|
+
top: pendingAnnotation.boundingBox.y - scrollOffset.y,
|
|
2221
|
+
width: pendingAnnotation.boundingBox.width,
|
|
2222
|
+
height: pendingAnnotation.boundingBox.height,
|
|
2223
|
+
border: `2px solid ${pendingAnnotation.isMultiSelect ? "#34C759" : "#3b82f6"}`,
|
|
2224
|
+
backgroundColor: pendingAnnotation.isMultiSelect ? "rgba(52, 199, 89, 0.1)" : "rgba(59, 130, 246, 0.1)",
|
|
2225
|
+
borderRadius: 4,
|
|
2226
|
+
pointerEvents: "none",
|
|
2227
|
+
zIndex: zIndex + 2,
|
|
2228
|
+
transition: "opacity 0.15s ease",
|
|
2229
|
+
opacity: pendingExiting ? 0 : 1
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
),
|
|
2233
|
+
/* @__PURE__ */ jsx(
|
|
2234
|
+
AnnotationPopup,
|
|
2235
|
+
{
|
|
2236
|
+
ref: popupRef,
|
|
2237
|
+
element: pendingAnnotation.element,
|
|
2238
|
+
selectedText: pendingAnnotation.selectedText,
|
|
2239
|
+
placeholder: pendingAnnotation.annotationType === "drawing" ? "What does this drawing mean?" : pendingAnnotation.isMultiSelect ? "What should change about these elements?" : "What should change?",
|
|
2240
|
+
onSubmit: handleAnnotationSubmit,
|
|
2241
|
+
onCancel: handleAnnotationCancel,
|
|
2242
|
+
isExiting: pendingExiting,
|
|
2243
|
+
isMultiSelect: pendingAnnotation.isMultiSelect,
|
|
2244
|
+
accentColor: pendingAnnotation.isMultiSelect ? "#34C759" : "#3b82f6",
|
|
2245
|
+
style: {
|
|
2246
|
+
// Position popup centered horizontally
|
|
2247
|
+
left: Math.max(
|
|
2248
|
+
160,
|
|
2249
|
+
Math.min(
|
|
2250
|
+
window.innerWidth - 160,
|
|
2251
|
+
pendingAnnotation.x / 100 * window.innerWidth
|
|
2252
|
+
)
|
|
2253
|
+
),
|
|
2254
|
+
// Position above or below based on viewport space
|
|
2255
|
+
...pendingAnnotation.clientY > window.innerHeight - 250 ? { bottom: window.innerHeight - pendingAnnotation.clientY + 30 } : { top: pendingAnnotation.clientY + 30 },
|
|
2256
|
+
zIndex: zIndex + 3,
|
|
2257
|
+
pointerEvents: "auto"
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
)
|
|
2261
|
+
] }),
|
|
2262
|
+
/* @__PURE__ */ jsx(
|
|
2263
|
+
AnnotationsSidebar,
|
|
2264
|
+
{
|
|
2265
|
+
annotations,
|
|
2266
|
+
onClear: handleClear,
|
|
2267
|
+
onExport: handleExport
|
|
2268
|
+
}
|
|
2269
|
+
),
|
|
2270
|
+
/* @__PURE__ */ jsxs(
|
|
2271
|
+
"div",
|
|
2272
|
+
{
|
|
2273
|
+
"data-skema": "toggle-hint",
|
|
2274
|
+
style: {
|
|
2275
|
+
position: "fixed",
|
|
2276
|
+
bottom: 16,
|
|
2277
|
+
left: 16,
|
|
2278
|
+
padding: "8px 12px",
|
|
2279
|
+
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
2280
|
+
color: "white",
|
|
2281
|
+
borderRadius: "6px",
|
|
2282
|
+
fontSize: "12px",
|
|
2283
|
+
pointerEvents: "none",
|
|
2284
|
+
zIndex: zIndex + 1
|
|
2285
|
+
},
|
|
2286
|
+
children: [
|
|
2287
|
+
"Press ",
|
|
2288
|
+
/* @__PURE__ */ jsx("kbd", { style: {
|
|
2289
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
2290
|
+
padding: "2px 6px",
|
|
2291
|
+
borderRadius: "3px",
|
|
2292
|
+
marginLeft: "4px",
|
|
2293
|
+
marginRight: "4px"
|
|
2294
|
+
}, children: "\u2318\u21E7E" }),
|
|
2295
|
+
" to toggle Skema"
|
|
2296
|
+
]
|
|
2297
|
+
}
|
|
2298
|
+
)
|
|
2299
|
+
]
|
|
2300
|
+
}
|
|
2301
|
+
);
|
|
2302
|
+
};
|
|
2303
|
+
var Skema_default = Skema;
|
|
2304
|
+
|
|
2305
|
+
export { AnnotationPopup, Skema, addGridToSvg, bboxCenter, bboxDocumentToViewport, bboxFromPoints, bboxIntersects, bboxViewportToDocument, blobToBase64, createDOMSelection, Skema_default as default, documentToViewport, expandBbox, extractTextFromShapes, generateSelector, getBoundingBox, getElementClasses, getElementPath, getGridCellReference, getViewportInfo, identifyElement, pointInBbox, shouldIgnoreElement, viewportToDocument };
|
|
2306
|
+
//# sourceMappingURL=index.mjs.map
|
|
2307
|
+
//# sourceMappingURL=index.mjs.map
|