@sylergydigital/issue-pin-sdk 0.6.4 → 0.6.6

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
@@ -157,6 +157,7 @@ interface FeedbackContextType {
157
157
  submitThread: (body: string, visibility: "public" | "internal") => Promise<void>;
158
158
  submitScreenshotThread: (body: string, visibility: "public" | "internal") => Promise<void>;
159
159
  refreshThreads: () => Promise<void>;
160
+ openThreadInDashboard: (threadId: string) => void;
160
161
  }
161
162
  /**
162
163
  * SDK Entry Point — wrap your app to enable in-app feedback.
package/dist/index.d.ts CHANGED
@@ -157,6 +157,7 @@ interface FeedbackContextType {
157
157
  submitThread: (body: string, visibility: "public" | "internal") => Promise<void>;
158
158
  submitScreenshotThread: (body: string, visibility: "public" | "internal") => Promise<void>;
159
159
  refreshThreads: () => Promise<void>;
160
+ openThreadInDashboard: (threadId: string) => void;
160
161
  }
161
162
  /**
162
163
  * SDK Entry Point — wrap your app to enable in-app feedback.
package/dist/index.js CHANGED
@@ -437,6 +437,34 @@ function FeedbackProvider({
437
437
  const onModeChangeUnified = config.onModeChange ?? (config.onFeedbackActiveChange ? ((m) => config.onFeedbackActiveChange(m === "annotate")) : void 0);
438
438
  const controlledModeFromProps = config.mode !== void 0 ? config.mode : config.feedbackActive !== void 0 ? config.feedbackActive ? "annotate" : "view" : void 0;
439
439
  const initialModeUncontrolled = config.mode ?? (config.feedbackActive !== void 0 ? config.feedbackActive ? "annotate" : "view" : "view");
440
+ const openThreadInDashboard = useCallback((threadId) => {
441
+ const threadPath = `/threads/${threadId}`;
442
+ const baseUrl = resolved.siteUrl?.replace(/\/+$/, "") || window.location.origin;
443
+ if (config.apiKey && autoIdentity.accessToken) {
444
+ const functionsBaseUrl = getFunctionsBaseUrl(resolved.supabaseUrl);
445
+ fetch(`${functionsBaseUrl}/external-sso-launch`, {
446
+ method: "POST",
447
+ headers: {
448
+ "Content-Type": "application/json",
449
+ Authorization: `Bearer ${autoIdentity.accessToken}`
450
+ },
451
+ body: JSON.stringify({
452
+ apiKey: config.apiKey,
453
+ nextPath: threadPath
454
+ })
455
+ }).then((res) => res.ok ? res.json() : null).then((data) => {
456
+ if (data?.redirect_url) {
457
+ window.open(data.redirect_url, "_blank", "noopener,noreferrer");
458
+ } else {
459
+ window.open(`${baseUrl}${threadPath}`, "_blank", "noopener,noreferrer");
460
+ }
461
+ }).catch(() => {
462
+ window.open(`${baseUrl}${threadPath}`, "_blank", "noopener,noreferrer");
463
+ });
464
+ return;
465
+ }
466
+ window.open(`${baseUrl}${threadPath}`, "_blank", "noopener,noreferrer");
467
+ }, [resolved.supabaseUrl, resolved.siteUrl, config.apiKey, autoIdentity.accessToken]);
440
468
  return /* @__PURE__ */ jsx(
441
469
  FeedbackProviderInner,
442
470
  {
@@ -463,6 +491,7 @@ function FeedbackProvider({
463
491
  userId: effectiveUserId,
464
492
  userEmail: effectiveEmail,
465
493
  userDisplayName: effectiveDisplayName,
494
+ openThreadInDashboard,
466
495
  children
467
496
  }
468
497
  );
@@ -491,7 +520,8 @@ function FeedbackProviderInner({
491
520
  debug,
492
521
  userId,
493
522
  userEmail,
494
- userDisplayName
523
+ userDisplayName,
524
+ openThreadInDashboard
495
525
  }) {
496
526
  const debugLog = useCallback((message, extra) => {
497
527
  if (!debug) return;
@@ -882,7 +912,8 @@ function FeedbackProviderInner({
882
912
  setPendingScreenshotPin,
883
913
  submitThread,
884
914
  submitScreenshotThread,
885
- refreshThreads: fetchThreads
915
+ refreshThreads: fetchThreads,
916
+ openThreadInDashboard
886
917
  },
887
918
  children: [
888
919
  children,
@@ -1242,7 +1273,7 @@ function ThreadPins() {
1242
1273
  const signedUrlCache = useRef2({});
1243
1274
  const threads = ctx?.threads ?? EMPTY_THREADS;
1244
1275
  const client = ctx?.client;
1245
- const threadBaseUrl = ctx?.siteUrl?.replace(/\/+$/, "") || window.location.origin;
1276
+ const openThreadInDashboard = ctx?.openThreadInDashboard;
1246
1277
  const scrollContainer = ctx?.scrollContainer;
1247
1278
  const container = scrollContainer?.ref.current ?? null;
1248
1279
  const getSignedUrl = useCallback4(async (path) => {
@@ -1366,8 +1397,8 @@ function ThreadPins() {
1366
1397
  });
1367
1398
  return;
1368
1399
  }
1369
- window.open(`${threadBaseUrl}/threads/${pin.threadId}`, "_blank", "noopener,noreferrer");
1370
- }, [getSignedUrl, threadBaseUrl]);
1400
+ openThreadInDashboard?.(pin.threadId);
1401
+ }, [getSignedUrl, openThreadInDashboard]);
1371
1402
  const containerLayer = useMemo3(() => {
1372
1403
  if (!container || containerPositions.length === 0) return null;
1373
1404
  const { width, height } = getContainerContentSize(container);
@@ -1494,7 +1525,7 @@ function ReviewSurfaceOverlay() {
1494
1525
  const mode = ctx?.mode ?? "view";
1495
1526
  const reviewUrl = ctx?.reviewUrl ?? null;
1496
1527
  const threads = ctx?.threads ?? EMPTY_THREADS2;
1497
- const threadBaseUrl = ctx?.siteUrl?.replace(/\/+$/, "") || window.location.origin;
1528
+ const openThreadInDashboard = ctx?.openThreadInDashboard;
1498
1529
  const updateMetrics = useCallback5(() => {
1499
1530
  const iframe = iframeRef.current;
1500
1531
  if (!iframe) return;
@@ -1672,7 +1703,7 @@ function ReviewSurfaceOverlay() {
1672
1703
  index: index + 1,
1673
1704
  left,
1674
1705
  top,
1675
- onClick: () => window.open(`${threadBaseUrl}/threads/${thread.id}`, "_blank", "noopener,noreferrer")
1706
+ onClick: () => openThreadInDashboard?.(thread.id)
1676
1707
  },
1677
1708
  thread.id
1678
1709
  );
@@ -2096,7 +2127,7 @@ function ScreenshotFeedback() {
2096
2127
  }
2097
2128
 
2098
2129
  // src/FeedbackButton.tsx
2099
- import { useRef as useRef5, useEffect as useEffect6, useState as useState7 } from "react";
2130
+ import { useRef as useRef5, useEffect as useEffect6, useState as useState7, useCallback as useCallback8 } from "react";
2100
2131
  import { MessageSquarePlus, MapPin, Camera as Camera2 } from "lucide-react";
2101
2132
 
2102
2133
  // src/launcher.ts
@@ -2118,10 +2149,52 @@ function getLauncherCapabilities({
2118
2149
 
2119
2150
  // src/FeedbackButton.tsx
2120
2151
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2152
+ var LAUNCHER_POS_KEY = "issue-pin:launcher-pos";
2153
+ var DRAG_THRESHOLD = 5;
2154
+ var EDGE_MARGIN = 20;
2155
+ var BTN_SIZE = 48;
2156
+ var SNAP_EASING = "cubic-bezier(0.2, 0, 0, 1)";
2157
+ var SNAP_TRANSITION = `left 0.3s ${SNAP_EASING}, top 0.3s ${SNAP_EASING}`;
2158
+ function loadPos(initialEdge) {
2159
+ try {
2160
+ const raw = localStorage.getItem(LAUNCHER_POS_KEY);
2161
+ if (raw) {
2162
+ const parsed = JSON.parse(raw);
2163
+ if ((parsed.edge === "left" || parsed.edge === "right") && typeof parsed.y === "number") {
2164
+ return { edge: parsed.edge, y: parsed.y };
2165
+ }
2166
+ }
2167
+ } catch {
2168
+ }
2169
+ return {
2170
+ edge: initialEdge === "bottom-left" ? "left" : "right",
2171
+ y: window.innerHeight - BTN_SIZE - EDGE_MARGIN
2172
+ };
2173
+ }
2174
+ function savePos(pos) {
2175
+ try {
2176
+ localStorage.setItem(LAUNCHER_POS_KEY, JSON.stringify(pos));
2177
+ } catch {
2178
+ }
2179
+ }
2180
+ function clampY(y) {
2181
+ return Math.min(
2182
+ Math.max(y, EDGE_MARGIN),
2183
+ window.innerHeight - BTN_SIZE - EDGE_MARGIN
2184
+ );
2185
+ }
2186
+ function snapXForEdge(edge) {
2187
+ return edge === "left" ? EDGE_MARGIN : window.innerWidth - BTN_SIZE - EDGE_MARGIN;
2188
+ }
2121
2189
  function FeedbackButton({ position = "bottom-right" }) {
2122
2190
  const ctx = useFeedbackSafe();
2123
2191
  const menuRef = useRef5(null);
2124
2192
  const [hintDismissed, setHintDismissed] = useState7(false);
2193
+ const [pos, setPos] = useState7(() => loadPos(position));
2194
+ const [dragXY, setDragXY] = useState7(null);
2195
+ const [snapping, setSnapping] = useState7(false);
2196
+ const dragStateRef = useRef5(null);
2197
+ const snapTimerRef = useRef5(null);
2125
2198
  const menuOpenState = ctx?.menuOpen ?? false;
2126
2199
  const debug = ctx?.debug ?? false;
2127
2200
  useEffect6(() => {
@@ -2139,6 +2212,79 @@ function FeedbackButton({ position = "bottom-right" }) {
2139
2212
  console.log("[EW SDK] FeedbackButton mounted");
2140
2213
  return () => console.log("[EW SDK] FeedbackButton unmounted");
2141
2214
  }, [debug]);
2215
+ useEffect6(() => {
2216
+ const handleResize = () => {
2217
+ setPos((prev) => {
2218
+ const newPos = {
2219
+ edge: prev.edge,
2220
+ y: clampY(prev.y)
2221
+ };
2222
+ savePos(newPos);
2223
+ return newPos;
2224
+ });
2225
+ };
2226
+ window.addEventListener("resize", handleResize);
2227
+ return () => window.removeEventListener("resize", handleResize);
2228
+ }, []);
2229
+ useEffect6(() => {
2230
+ return () => {
2231
+ if (snapTimerRef.current) clearTimeout(snapTimerRef.current);
2232
+ };
2233
+ }, []);
2234
+ const handlePointerDown = useCallback8((e) => {
2235
+ if (e.button != null && e.button !== 0) return;
2236
+ e.currentTarget.setPointerCapture(e.pointerId);
2237
+ dragStateRef.current = {
2238
+ active: false,
2239
+ startX: e.clientX,
2240
+ startY: e.clientY,
2241
+ currentX: e.clientX,
2242
+ currentY: e.clientY,
2243
+ didDrag: false
2244
+ };
2245
+ }, []);
2246
+ const handlePointerMove = useCallback8((e) => {
2247
+ const ds = dragStateRef.current;
2248
+ if (!ds) return;
2249
+ const dx = e.clientX - ds.startX;
2250
+ const dy = e.clientY - ds.startY;
2251
+ const distance = Math.sqrt(dx * dx + dy * dy);
2252
+ if (!ds.active && distance >= DRAG_THRESHOLD) {
2253
+ ds.active = true;
2254
+ ds.didDrag = true;
2255
+ }
2256
+ if (ds.active) {
2257
+ ds.currentX = e.clientX;
2258
+ ds.currentY = e.clientY;
2259
+ const x = e.clientX - BTN_SIZE / 2;
2260
+ const y = clampY(e.clientY - BTN_SIZE / 2);
2261
+ setDragXY({ x, y });
2262
+ }
2263
+ }, []);
2264
+ const handlePointerUp = useCallback8((e) => {
2265
+ try {
2266
+ e.currentTarget.releasePointerCapture(e.pointerId);
2267
+ } catch {
2268
+ }
2269
+ const ds = dragStateRef.current;
2270
+ if (!ds || !ds.didDrag) {
2271
+ dragStateRef.current = null;
2272
+ return;
2273
+ }
2274
+ const centerX = ds.currentX;
2275
+ const edge = centerX < window.innerWidth / 2 ? "left" : "right";
2276
+ const y = clampY(ds.currentY - BTN_SIZE / 2);
2277
+ const newPos = { edge, y };
2278
+ setSnapping(true);
2279
+ setPos(newPos);
2280
+ savePos(newPos);
2281
+ setDragXY(null);
2282
+ if (snapTimerRef.current) clearTimeout(snapTimerRef.current);
2283
+ snapTimerRef.current = setTimeout(() => {
2284
+ setSnapping(false);
2285
+ snapTimerRef.current = null;
2286
+ }, 300);
2287
+ }, []);
2142
2288
  if (!ctx) return null;
2143
2289
  const {
2144
2290
  mode,
@@ -2154,9 +2300,13 @@ function FeedbackButton({ position = "bottom-right" }) {
2154
2300
  } = ctx;
2155
2301
  const annotate = mode === "annotate";
2156
2302
  const capabilities = getLauncherCapabilities({ allowPinOnPage: canPinOnPage, allowScreenshot: canScreenshot });
2157
- const posStyle = position === "bottom-left" ? { left: 20, bottom: 20 } : { right: 20, bottom: 20 };
2158
- const hintStyle = position === "bottom-left" ? { left: 56, bottom: 8 } : { right: 56, bottom: 8 };
2159
2303
  const handleToggle = () => {
2304
+ if (dragStateRef.current?.didDrag) {
2305
+ dragStateRef.current.didDrag = false;
2306
+ dragStateRef.current = null;
2307
+ return;
2308
+ }
2309
+ dragStateRef.current = null;
2160
2310
  if (debug) console.log("[EW SDK] handleToggle", { mode, menuOpen });
2161
2311
  setHintDismissed(true);
2162
2312
  if (annotate) {
@@ -2176,12 +2326,44 @@ function FeedbackButton({ position = "bottom-right" }) {
2176
2326
  const capturing = screenshotCapturing;
2177
2327
  const showLauncherHint = showHints && !hintDismissed && !annotate && !menuOpen;
2178
2328
  if (capabilities.actionCount === 0) return null;
2329
+ const restX = snapXForEdge(pos.edge);
2330
+ let wrapperStyle;
2331
+ if (dragXY !== null) {
2332
+ wrapperStyle = {
2333
+ position: "fixed",
2334
+ left: dragXY.x,
2335
+ top: dragXY.y,
2336
+ transition: "none",
2337
+ zIndex: Z.launcher
2338
+ };
2339
+ } else if (snapping) {
2340
+ wrapperStyle = {
2341
+ position: "fixed",
2342
+ left: restX,
2343
+ top: pos.y,
2344
+ transition: SNAP_TRANSITION,
2345
+ zIndex: Z.launcher
2346
+ };
2347
+ } else {
2348
+ wrapperStyle = {
2349
+ position: "fixed",
2350
+ left: restX,
2351
+ top: pos.y,
2352
+ transition: "none",
2353
+ zIndex: Z.launcher
2354
+ };
2355
+ }
2356
+ const hintStyle = pos.edge === "right" ? { right: BTN_SIZE + 8, top: "50%", transform: "translateY(-50%)" } : { left: BTN_SIZE + 8, top: "50%", transform: "translateY(-50%)" };
2357
+ const menuPositionStyle = pos.edge === "right" ? { right: 0, bottom: BTN_SIZE + 4 } : { left: 0, bottom: BTN_SIZE + 4 };
2179
2358
  return /* @__PURE__ */ jsxs6(
2180
2359
  "div",
2181
2360
  {
2182
2361
  ref: menuRef,
2183
2362
  "data-ew-feedback-interactive": "true",
2184
- style: { position: "fixed", ...posStyle, zIndex: Z.launcher },
2363
+ style: { ...wrapperStyle, touchAction: "none" },
2364
+ onPointerDown: handlePointerDown,
2365
+ onPointerMove: handlePointerMove,
2366
+ onPointerUp: handlePointerUp,
2185
2367
  children: [
2186
2368
  showLauncherHint && /* @__PURE__ */ jsxs6(
2187
2369
  "div",
@@ -2208,8 +2390,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2208
2390
  {
2209
2391
  style: {
2210
2392
  position: "absolute",
2211
- bottom: 56,
2212
- right: 0,
2393
+ ...menuPositionStyle,
2213
2394
  marginBottom: 4,
2214
2395
  width: 176,
2215
2396
  borderRadius: 8,
@@ -2274,7 +2455,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2274
2455
  children: [
2275
2456
  /* @__PURE__ */ jsx7(Camera2, { style: { width: 16, height: 16, color: T.primary, flexShrink: 0 } }),
2276
2457
  /* @__PURE__ */ jsxs6("div", { children: [
2277
- /* @__PURE__ */ jsx7("div", { style: { fontWeight: 500 }, children: capturing ? "Capturing\u2026" : "Screenshot" }),
2458
+ /* @__PURE__ */ jsx7("div", { style: { fontWeight: 500 }, children: capturing ? "Capturing..." : "Screenshot" }),
2278
2459
  /* @__PURE__ */ jsx7("div", { style: { fontSize: 10, color: T.muted }, children: "Capture current view" })
2279
2460
  ] })
2280
2461
  ]
@@ -2289,8 +2470,8 @@ function FeedbackButton({ position = "bottom-right" }) {
2289
2470
  onClick: handleToggle,
2290
2471
  style: {
2291
2472
  display: "flex",
2292
- height: 48,
2293
- width: 48,
2473
+ height: BTN_SIZE,
2474
+ width: BTN_SIZE,
2294
2475
  alignItems: "center",
2295
2476
  justifyContent: "center",
2296
2477
  borderRadius: "50%",
@@ -2299,7 +2480,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2299
2480
  border: annotate ? "none" : `1px solid ${T.border}`,
2300
2481
  background: annotate ? T.primary : T.card,
2301
2482
  color: annotate ? T.primaryFg : T.fg,
2302
- cursor: "pointer",
2483
+ cursor: dragXY !== null ? "grabbing" : "pointer",
2303
2484
  transform: annotate ? "scale(1.1)" : "scale(1)"
2304
2485
  },
2305
2486
  title: annotate ? "Exit feedback mode" : "Open feedback menu",
@@ -2313,7 +2494,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2313
2494
  }
2314
2495
 
2315
2496
  // src/ModalFeedbackInjector.tsx
2316
- import { useState as useState8, useEffect as useEffect7, useCallback as useCallback8 } from "react";
2497
+ import { useState as useState8, useEffect as useEffect7, useCallback as useCallback9 } from "react";
2317
2498
  import { createPortal as createPortal3 } from "react-dom";
2318
2499
  import { Camera as Camera3 } from "lucide-react";
2319
2500
  import { Fragment as Fragment4, jsx as jsx8 } from "react/jsx-runtime";
@@ -2332,7 +2513,7 @@ function ModalFeedbackInjector({
2332
2513
  observer.observe(document.body, { childList: true, subtree: true });
2333
2514
  return () => observer.disconnect();
2334
2515
  }, []);
2335
- const handleCapture = useCallback8(async () => {
2516
+ const handleCapture = useCallback9(async () => {
2336
2517
  if (!ctx) return;
2337
2518
  await ctx.startScreenshotCapture();
2338
2519
  }, [ctx]);