@useclickly/react 1.2.0 → 1.3.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/dist/index.cjs +185 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +185 -89
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as zustand_middleware from 'zustand/middleware';
|
|
1
2
|
import * as zustand from 'zustand';
|
|
2
3
|
import { Annotation } from '@useclickly/core';
|
|
3
4
|
export { Annotation, AnnotationKind, AnnotationStatus, ScreenshotData } from '@useclickly/core';
|
|
@@ -49,7 +50,19 @@ interface AnnotationsStore {
|
|
|
49
50
|
* a save back — these annotations already exist on disk. */
|
|
50
51
|
hydrateStrokes: (strokesById: Record<string, NonNullable<Annotation["strokes"]>>) => void;
|
|
51
52
|
}
|
|
52
|
-
declare const useAnnotations: zustand.UseBoundStore<zustand.StoreApi<AnnotationsStore
|
|
53
|
+
declare const useAnnotations: zustand.UseBoundStore<Omit<zustand.StoreApi<AnnotationsStore>, "setState" | "persist"> & {
|
|
54
|
+
setState(partial: AnnotationsStore | Partial<AnnotationsStore> | ((state: AnnotationsStore) => AnnotationsStore | Partial<AnnotationsStore>), replace?: false | undefined): unknown;
|
|
55
|
+
setState(state: AnnotationsStore | ((state: AnnotationsStore) => AnnotationsStore), replace: true): unknown;
|
|
56
|
+
persist: {
|
|
57
|
+
setOptions: (options: Partial<zustand_middleware.PersistOptions<AnnotationsStore, unknown, unknown>>) => void;
|
|
58
|
+
clearStorage: () => void;
|
|
59
|
+
rehydrate: () => Promise<void> | void;
|
|
60
|
+
hasHydrated: () => boolean;
|
|
61
|
+
onHydrate: (fn: (state: AnnotationsStore) => void) => () => void;
|
|
62
|
+
onFinishHydration: (fn: (state: AnnotationsStore) => void) => () => void;
|
|
63
|
+
getOptions: () => Partial<zustand_middleware.PersistOptions<AnnotationsStore, unknown, unknown>>;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
53
66
|
/**
|
|
54
67
|
* Subscribe to the annotation list with shallow equality — re-renders
|
|
55
68
|
* only when items add/remove/reorder/update. Always use this from
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as zustand_middleware from 'zustand/middleware';
|
|
1
2
|
import * as zustand from 'zustand';
|
|
2
3
|
import { Annotation } from '@useclickly/core';
|
|
3
4
|
export { Annotation, AnnotationKind, AnnotationStatus, ScreenshotData } from '@useclickly/core';
|
|
@@ -49,7 +50,19 @@ interface AnnotationsStore {
|
|
|
49
50
|
* a save back — these annotations already exist on disk. */
|
|
50
51
|
hydrateStrokes: (strokesById: Record<string, NonNullable<Annotation["strokes"]>>) => void;
|
|
51
52
|
}
|
|
52
|
-
declare const useAnnotations: zustand.UseBoundStore<zustand.StoreApi<AnnotationsStore
|
|
53
|
+
declare const useAnnotations: zustand.UseBoundStore<Omit<zustand.StoreApi<AnnotationsStore>, "setState" | "persist"> & {
|
|
54
|
+
setState(partial: AnnotationsStore | Partial<AnnotationsStore> | ((state: AnnotationsStore) => AnnotationsStore | Partial<AnnotationsStore>), replace?: false | undefined): unknown;
|
|
55
|
+
setState(state: AnnotationsStore | ((state: AnnotationsStore) => AnnotationsStore), replace: true): unknown;
|
|
56
|
+
persist: {
|
|
57
|
+
setOptions: (options: Partial<zustand_middleware.PersistOptions<AnnotationsStore, unknown, unknown>>) => void;
|
|
58
|
+
clearStorage: () => void;
|
|
59
|
+
rehydrate: () => Promise<void> | void;
|
|
60
|
+
hasHydrated: () => boolean;
|
|
61
|
+
onHydrate: (fn: (state: AnnotationsStore) => void) => () => void;
|
|
62
|
+
onFinishHydration: (fn: (state: AnnotationsStore) => void) => () => void;
|
|
63
|
+
getOptions: () => Partial<zustand_middleware.PersistOptions<AnnotationsStore, unknown, unknown>>;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
53
66
|
/**
|
|
54
67
|
* Subscribe to the annotation list with shallow equality — re-renders
|
|
55
68
|
* only when items add/remove/reorder/update. Always use this from
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useState, useEffect, useSyncExternalStore, useRef, useCallback } from '
|
|
|
3
3
|
import { createRoot } from 'react-dom/client';
|
|
4
4
|
import { isPlacementAnnotation, isRearrangeAnnotation, createShadowHost, SelectionEngine, Overlay, collectComputedStyles, collectMetadata, getReadableElementPath, identifyElement } from '@useclickly/core';
|
|
5
5
|
import { create } from 'zustand';
|
|
6
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
6
7
|
import { useShallow } from 'zustand/react/shallow';
|
|
7
8
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
9
|
import { nanoid } from 'nanoid';
|
|
@@ -144,52 +145,84 @@ async function loadDirHandle() {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
// src/state/annotations.ts
|
|
147
|
-
var
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
148
|
+
var STORAGE_KEY = "clickly:annotations";
|
|
149
|
+
var useAnnotations = create()(
|
|
150
|
+
persist(
|
|
151
|
+
(set, get) => ({
|
|
152
|
+
byId: {},
|
|
153
|
+
order: [],
|
|
154
|
+
add: (a) => {
|
|
155
|
+
if (a.strokes && a.strokes.length > 0) void saveStrokes(a.id, a.strokes);
|
|
156
|
+
set((s) => ({
|
|
157
|
+
byId: { ...s.byId, [a.id]: a },
|
|
158
|
+
order: s.order.includes(a.id) ? s.order : [...s.order, a.id]
|
|
159
|
+
}));
|
|
160
|
+
},
|
|
161
|
+
remove: (id) => {
|
|
162
|
+
void deleteStrokes(id);
|
|
163
|
+
set((s) => {
|
|
164
|
+
if (!(id in s.byId)) return s;
|
|
165
|
+
const next = { ...s.byId };
|
|
166
|
+
delete next[id];
|
|
167
|
+
return { byId: next, order: s.order.filter((x) => x !== id) };
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
update: (id, patch) => {
|
|
171
|
+
if (patch.strokes) void saveStrokes(id, patch.strokes);
|
|
172
|
+
set((s) => {
|
|
173
|
+
const cur = s.byId[id];
|
|
174
|
+
if (!cur) return s;
|
|
175
|
+
return { byId: { ...s.byId, [id]: { ...cur, ...patch } } };
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
clear: () => {
|
|
179
|
+
set({ byId: {}, order: [] });
|
|
180
|
+
},
|
|
181
|
+
list: () => {
|
|
182
|
+
const { byId, order } = get();
|
|
183
|
+
return order.map((id) => byId[id]).filter(Boolean);
|
|
184
|
+
},
|
|
185
|
+
hydrateStrokes: (strokesById) => set((s) => {
|
|
186
|
+
let touched = false;
|
|
187
|
+
const next = { ...s.byId };
|
|
188
|
+
for (const [id, strokes] of Object.entries(strokesById)) {
|
|
189
|
+
const cur = next[id];
|
|
190
|
+
if (!cur) continue;
|
|
191
|
+
next[id] = { ...cur, strokes };
|
|
192
|
+
touched = true;
|
|
193
|
+
}
|
|
194
|
+
return touched ? { byId: next } : s;
|
|
195
|
+
})
|
|
196
|
+
}),
|
|
197
|
+
{
|
|
198
|
+
name: STORAGE_KEY,
|
|
199
|
+
storage: createJSONStorage(() => {
|
|
200
|
+
if (typeof localStorage !== "undefined") return localStorage;
|
|
201
|
+
return {
|
|
202
|
+
getItem: () => null,
|
|
203
|
+
setItem: () => void 0,
|
|
204
|
+
removeItem: () => void 0
|
|
205
|
+
};
|
|
206
|
+
}),
|
|
207
|
+
// Skip strokes — they live in IndexedDB and are reattached by
|
|
208
|
+
// hydrateStrokes(). Persisting them in JSON would blow the
|
|
209
|
+
// localStorage quota (5MB) on a single freehand drawing.
|
|
210
|
+
partialize: (state) => ({
|
|
211
|
+
byId: Object.fromEntries(
|
|
212
|
+
Object.entries(state.byId).map(([id, ann]) => {
|
|
213
|
+
const { strokes: _strokes, ...rest } = ann;
|
|
214
|
+
return [id, rest];
|
|
215
|
+
})
|
|
216
|
+
),
|
|
217
|
+
order: state.order
|
|
218
|
+
}),
|
|
219
|
+
// Bump this if the persisted shape changes; old payloads are
|
|
220
|
+
// ignored and the store starts empty rather than crashing on
|
|
221
|
+
// a mismatched schema.
|
|
222
|
+
version: 1
|
|
189
223
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}));
|
|
224
|
+
)
|
|
225
|
+
);
|
|
193
226
|
function useAnnotationsList() {
|
|
194
227
|
return useAnnotations(
|
|
195
228
|
useShallow((s) => s.order.map((id) => s.byId[id]).filter(Boolean))
|
|
@@ -205,11 +238,11 @@ var DEFAULTS = {
|
|
|
205
238
|
mcpEndpoint: "http://localhost:4747",
|
|
206
239
|
mcpSessionId: null
|
|
207
240
|
};
|
|
208
|
-
var
|
|
241
|
+
var STORAGE_KEY2 = "clickly:settings";
|
|
209
242
|
function load() {
|
|
210
243
|
if (typeof localStorage === "undefined") return DEFAULTS;
|
|
211
244
|
try {
|
|
212
|
-
const raw = localStorage.getItem(
|
|
245
|
+
const raw = localStorage.getItem(STORAGE_KEY2);
|
|
213
246
|
if (!raw) return DEFAULTS;
|
|
214
247
|
const parsed = JSON.parse(raw);
|
|
215
248
|
return { ...DEFAULTS, ...parsed };
|
|
@@ -217,10 +250,10 @@ function load() {
|
|
|
217
250
|
return DEFAULTS;
|
|
218
251
|
}
|
|
219
252
|
}
|
|
220
|
-
function
|
|
253
|
+
function persist2(s) {
|
|
221
254
|
if (typeof localStorage === "undefined") return;
|
|
222
255
|
try {
|
|
223
|
-
localStorage.setItem(
|
|
256
|
+
localStorage.setItem(STORAGE_KEY2, JSON.stringify(s));
|
|
224
257
|
} catch {
|
|
225
258
|
}
|
|
226
259
|
}
|
|
@@ -228,11 +261,11 @@ var useSettings = create((set) => ({
|
|
|
228
261
|
...load(),
|
|
229
262
|
set: (patch) => set((cur) => {
|
|
230
263
|
const next = { ...cur, ...patch };
|
|
231
|
-
|
|
264
|
+
persist2(next);
|
|
232
265
|
return next;
|
|
233
266
|
}),
|
|
234
267
|
reset: () => {
|
|
235
|
-
|
|
268
|
+
persist2(DEFAULTS);
|
|
236
269
|
set(DEFAULTS);
|
|
237
270
|
}
|
|
238
271
|
}));
|
|
@@ -1366,6 +1399,11 @@ function downloadViaAnchor(dataUrl, filename) {
|
|
|
1366
1399
|
return false;
|
|
1367
1400
|
}
|
|
1368
1401
|
}
|
|
1402
|
+
var PANEL_W2 = 336;
|
|
1403
|
+
var PANEL_H2 = 420;
|
|
1404
|
+
var PANEL_GAP2 = 12;
|
|
1405
|
+
var VIEWPORT_PAD2 = 8;
|
|
1406
|
+
var TOOLBAR_H2 = 46;
|
|
1369
1407
|
function AnnotationList({ anchor, width, onClose }) {
|
|
1370
1408
|
const items = useAnnotationsList();
|
|
1371
1409
|
const remove = useAnnotations((s) => s.remove);
|
|
@@ -1379,9 +1417,17 @@ function AnnotationList({ anchor, width, onClose }) {
|
|
|
1379
1417
|
window.addEventListener("pointerdown", onDown, true);
|
|
1380
1418
|
return () => window.removeEventListener("pointerdown", onDown, true);
|
|
1381
1419
|
}, [onClose]);
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
1384
|
-
const
|
|
1420
|
+
const vw = typeof window !== "undefined" ? window.innerWidth : 1024;
|
|
1421
|
+
const vh = typeof window !== "undefined" ? window.innerHeight : 768;
|
|
1422
|
+
const spaceAbove = anchor.y - VIEWPORT_PAD2;
|
|
1423
|
+
const spaceBelow = vh - anchor.y - TOOLBAR_H2 - VIEWPORT_PAD2;
|
|
1424
|
+
const placeAbove = spaceAbove >= PANEL_H2 + PANEL_GAP2 || spaceAbove >= spaceBelow;
|
|
1425
|
+
const top = placeAbove ? Math.max(VIEWPORT_PAD2, anchor.y - PANEL_H2 - PANEL_GAP2) : Math.min(vh - PANEL_H2 - VIEWPORT_PAD2, anchor.y + TOOLBAR_H2 + PANEL_GAP2);
|
|
1426
|
+
const desiredLeft = anchor.x + width - PANEL_W2;
|
|
1427
|
+
const left = Math.min(
|
|
1428
|
+
vw - PANEL_W2 - VIEWPORT_PAD2,
|
|
1429
|
+
Math.max(VIEWPORT_PAD2, desiredLeft)
|
|
1430
|
+
);
|
|
1385
1431
|
const [exporting, setExporting] = useState(false);
|
|
1386
1432
|
const update = useAnnotations((s) => s.update);
|
|
1387
1433
|
const exportComposite = async () => {
|
|
@@ -1405,36 +1451,55 @@ function AnnotationList({ anchor, width, onClose }) {
|
|
|
1405
1451
|
setExporting(false);
|
|
1406
1452
|
}
|
|
1407
1453
|
};
|
|
1408
|
-
return /* @__PURE__ */ jsxs(
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1454
|
+
return /* @__PURE__ */ jsxs(
|
|
1455
|
+
"div",
|
|
1456
|
+
{
|
|
1457
|
+
ref,
|
|
1458
|
+
className: "clickly-list",
|
|
1459
|
+
style: { left, top, width: PANEL_W2, height: PANEL_H2 },
|
|
1460
|
+
children: [
|
|
1461
|
+
/* @__PURE__ */ jsxs("div", { className: "list-header", children: [
|
|
1462
|
+
/* @__PURE__ */ jsx("span", { className: "list-title", children: "Annotations" }),
|
|
1463
|
+
/* @__PURE__ */ jsx("span", { className: "list-count", children: items.length }),
|
|
1464
|
+
items.length > 0 && /* @__PURE__ */ jsx(
|
|
1465
|
+
"button",
|
|
1466
|
+
{
|
|
1467
|
+
className: "list-action-btn",
|
|
1468
|
+
onClick: exportComposite,
|
|
1469
|
+
title: "Export all screenshots as a numbered image strip",
|
|
1470
|
+
disabled: exporting,
|
|
1471
|
+
style: { marginLeft: "auto" },
|
|
1472
|
+
children: /* @__PURE__ */ jsx(IconDownload, {})
|
|
1473
|
+
}
|
|
1474
|
+
),
|
|
1475
|
+
/* @__PURE__ */ jsx(
|
|
1476
|
+
"button",
|
|
1477
|
+
{
|
|
1478
|
+
className: "list-action-btn",
|
|
1479
|
+
onClick: onClose,
|
|
1480
|
+
"aria-label": "Close annotation list",
|
|
1481
|
+
title: "Close",
|
|
1482
|
+
style: items.length > 0 ? void 0 : { marginLeft: "auto" },
|
|
1483
|
+
children: /* @__PURE__ */ jsx(IconClose, {})
|
|
1484
|
+
}
|
|
1485
|
+
)
|
|
1486
|
+
] }),
|
|
1487
|
+
/* @__PURE__ */ jsx("div", { className: "list-scroll", children: items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "list-empty", children: "No annotations yet." }) : /* @__PURE__ */ jsx("div", { className: "list-items", children: items.map((a, i) => /* @__PURE__ */ jsx(
|
|
1488
|
+
AnnotationCard,
|
|
1489
|
+
{
|
|
1490
|
+
annotation: a,
|
|
1491
|
+
index: i + 1,
|
|
1492
|
+
outputDetail,
|
|
1493
|
+
onRemove: () => remove(a.id),
|
|
1494
|
+
onCaptured: (data) => update(a.id, {
|
|
1495
|
+
screenshot: { mimeType: "image/jpeg", dataUrl: data, width: 0, height: 0 }
|
|
1496
|
+
})
|
|
1497
|
+
},
|
|
1498
|
+
a.id
|
|
1499
|
+
)) }) })
|
|
1500
|
+
]
|
|
1501
|
+
}
|
|
1502
|
+
);
|
|
1438
1503
|
}
|
|
1439
1504
|
function AnnotationCard({
|
|
1440
1505
|
annotation: a,
|
|
@@ -5402,9 +5467,11 @@ var REACT_UI_CSS = `
|
|
|
5402
5467
|
/* \u2500\u2500\u2500 Annotation list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
5403
5468
|
|
|
5404
5469
|
.clickly-list {
|
|
5470
|
+
/* Fixed-size frame. Width/height set inline by AnnotationList so the
|
|
5471
|
+
values stay in one place (TS source of truth). Header is pinned;
|
|
5472
|
+
.list-scroll fills the remainder and scrolls internally \u2014 matches
|
|
5473
|
+
the SettingsPopover layout. */
|
|
5405
5474
|
position: fixed;
|
|
5406
|
-
width: 320px;
|
|
5407
|
-
max-height: 55vh;
|
|
5408
5475
|
background: #fff;
|
|
5409
5476
|
border-radius: 14px;
|
|
5410
5477
|
box-shadow: 0 16px 48px rgba(2,6,23,0.20), 0 0 0 1px rgba(15,23,42,0.07);
|
|
@@ -5418,6 +5485,34 @@ var REACT_UI_CSS = `
|
|
|
5418
5485
|
overflow: hidden;
|
|
5419
5486
|
}
|
|
5420
5487
|
|
|
5488
|
+
/* Scrollable body \u2014 mirrors .settings-scroll. min-height:0 is the
|
|
5489
|
+
classic fix that lets a flex child shrink small enough to overflow
|
|
5490
|
+
inside its parent (otherwise the auto min-content size pushes the
|
|
5491
|
+
layout taller than the frame, defeating overflow:auto). */
|
|
5492
|
+
.list-scroll {
|
|
5493
|
+
flex: 1 1 auto;
|
|
5494
|
+
min-height: 0;
|
|
5495
|
+
overflow-y: auto;
|
|
5496
|
+
overscroll-behavior: contain;
|
|
5497
|
+
scrollbar-width: thin;
|
|
5498
|
+
scrollbar-color: rgba(15,23,42,0.20) transparent;
|
|
5499
|
+
}
|
|
5500
|
+
.list-scroll::-webkit-scrollbar { width: 6px; }
|
|
5501
|
+
.list-scroll::-webkit-scrollbar-thumb {
|
|
5502
|
+
background: rgba(15,23,42,0.18);
|
|
5503
|
+
border-radius: 3px;
|
|
5504
|
+
}
|
|
5505
|
+
.list-scroll::-webkit-scrollbar-thumb:hover { background: rgba(15,23,42,0.32); }
|
|
5506
|
+
:host([data-clickly-theme="dark"]) .list-scroll {
|
|
5507
|
+
scrollbar-color: rgba(255,255,255,0.18) transparent;
|
|
5508
|
+
}
|
|
5509
|
+
:host([data-clickly-theme="dark"]) .list-scroll::-webkit-scrollbar-thumb {
|
|
5510
|
+
background: rgba(255,255,255,0.18);
|
|
5511
|
+
}
|
|
5512
|
+
:host([data-clickly-theme="dark"]) .list-scroll::-webkit-scrollbar-thumb:hover {
|
|
5513
|
+
background: rgba(255,255,255,0.32);
|
|
5514
|
+
}
|
|
5515
|
+
|
|
5421
5516
|
.list-header {
|
|
5422
5517
|
display: flex;
|
|
5423
5518
|
align-items: center;
|
|
@@ -5449,9 +5544,10 @@ var REACT_UI_CSS = `
|
|
|
5449
5544
|
}
|
|
5450
5545
|
|
|
5451
5546
|
.list-items {
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5547
|
+
/* Cards container \u2014 scrolling moved up to .list-scroll so the
|
|
5548
|
+
empty state shares the same scroll region. */
|
|
5549
|
+
display: flex;
|
|
5550
|
+
flex-direction: column;
|
|
5455
5551
|
}
|
|
5456
5552
|
|
|
5457
5553
|
.list-empty {
|