@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.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 useAnnotations = create((set, get) => ({
148
- byId: {},
149
- order: [],
150
- add: (a) => {
151
- if (a.strokes && a.strokes.length > 0) void saveStrokes(a.id, a.strokes);
152
- set((s) => ({
153
- byId: { ...s.byId, [a.id]: a },
154
- order: s.order.includes(a.id) ? s.order : [...s.order, a.id]
155
- }));
156
- },
157
- remove: (id) => {
158
- void deleteStrokes(id);
159
- set((s) => {
160
- if (!(id in s.byId)) return s;
161
- const next = { ...s.byId };
162
- delete next[id];
163
- return { byId: next, order: s.order.filter((x) => x !== id) };
164
- });
165
- },
166
- update: (id, patch) => {
167
- if (patch.strokes) void saveStrokes(id, patch.strokes);
168
- set((s) => {
169
- const cur = s.byId[id];
170
- if (!cur) return s;
171
- return { byId: { ...s.byId, [id]: { ...cur, ...patch } } };
172
- });
173
- },
174
- clear: () => {
175
- set({ byId: {}, order: [] });
176
- },
177
- list: () => {
178
- const { byId, order } = get();
179
- return order.map((id) => byId[id]).filter(Boolean);
180
- },
181
- hydrateStrokes: (strokesById) => set((s) => {
182
- let touched = false;
183
- const next = { ...s.byId };
184
- for (const [id, strokes] of Object.entries(strokesById)) {
185
- const cur = next[id];
186
- if (!cur) continue;
187
- next[id] = { ...cur, strokes };
188
- touched = true;
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
- return touched ? { byId: next } : s;
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 STORAGE_KEY = "clickly:settings";
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(STORAGE_KEY);
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 persist(s) {
253
+ function persist2(s) {
221
254
  if (typeof localStorage === "undefined") return;
222
255
  try {
223
- localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
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
- persist(next);
264
+ persist2(next);
232
265
  return next;
233
266
  }),
234
267
  reset: () => {
235
- persist(DEFAULTS);
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 estimatedHeight = Math.min(window.innerHeight * 0.55, items.length * 88 + 48);
1383
- const top = Math.max(8, anchor.y - estimatedHeight - 12);
1384
- const left = Math.min(window.innerWidth - 336, Math.max(8, anchor.x));
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("div", { ref, className: "clickly-list", style: { left, top }, children: [
1409
- /* @__PURE__ */ jsxs("div", { className: "list-header", children: [
1410
- /* @__PURE__ */ jsx("span", { className: "list-title", children: "Annotations" }),
1411
- /* @__PURE__ */ jsx("span", { className: "list-count", children: items.length }),
1412
- items.length > 0 && /* @__PURE__ */ jsx(
1413
- "button",
1414
- {
1415
- className: "list-action-btn",
1416
- onClick: exportComposite,
1417
- title: "Export all screenshots as a numbered image strip",
1418
- disabled: exporting,
1419
- style: { marginLeft: "auto" },
1420
- children: /* @__PURE__ */ jsx(IconDownload, {})
1421
- }
1422
- )
1423
- ] }),
1424
- 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(
1425
- AnnotationCard,
1426
- {
1427
- annotation: a,
1428
- index: i + 1,
1429
- outputDetail,
1430
- onRemove: () => remove(a.id),
1431
- onCaptured: (data) => update(a.id, {
1432
- screenshot: { mimeType: "image/jpeg", dataUrl: data, width: 0, height: 0 }
1433
- })
1434
- },
1435
- a.id
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
- overflow-y: auto;
5453
- overscroll-behavior: contain;
5454
- flex: 1;
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 {