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