@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/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to `@sylergydigital/issue-pin-sdk` are documented here.
4
4
 
5
+ ## [0.6.6] - 2026-04-15
6
+
7
+ ### Fixed
8
+ - **SSO handoff for federated pin click-through** — clicking a pin on a 3rd-party app now auto-logs the user into the IssuePin dashboard via `external-sso-launch` magic link, instead of landing on the login page. Falls back to direct link when not in a federated context.
9
+
10
+ ## [0.6.5] - 2026-04-15
11
+
12
+ ### Added
13
+ - **Draggable launcher button** — the feedback launcher can now be freely dragged across the viewport using mouse, touch, or pen input (iOS AssistiveTouch pattern). On release, the button smoothly snaps to the nearest left or right viewport edge.
14
+ - Position persistence — launcher position (edge + Y offset) is saved to `localStorage` and restored across page reloads.
15
+ - Menu and hint toast auto-flip to the opposite side of the docked edge so they never render off-screen.
16
+ - Viewport resize clamping — button repositions to stay within bounds when the window is resized.
17
+ - Click vs. drag disambiguation — short interactions (< 5px movement) still toggle the menu normally.
18
+
19
+ ## [0.6.4] - 2026-04-14
20
+
21
+ ### Fixed
22
+ - Included `CHANGELOG.md` in the SDK npm publish pipeline (`files` array in package.json).
23
+
24
+ ### Added
25
+ - SDK CHANGELOG.md with full release history.
26
+
5
27
  ## [0.6.3] - 2026-04-14
6
28
 
7
29
  ### Added
package/dist/index.cjs CHANGED
@@ -487,6 +487,34 @@ function FeedbackProvider({
487
487
  const onModeChangeUnified = config.onModeChange ?? (config.onFeedbackActiveChange ? ((m) => config.onFeedbackActiveChange(m === "annotate")) : void 0);
488
488
  const controlledModeFromProps = config.mode !== void 0 ? config.mode : config.feedbackActive !== void 0 ? config.feedbackActive ? "annotate" : "view" : void 0;
489
489
  const initialModeUncontrolled = config.mode ?? (config.feedbackActive !== void 0 ? config.feedbackActive ? "annotate" : "view" : "view");
490
+ const openThreadInDashboard = (0, import_react2.useCallback)((threadId) => {
491
+ const threadPath = `/threads/${threadId}`;
492
+ const baseUrl = resolved.siteUrl?.replace(/\/+$/, "") || window.location.origin;
493
+ if (config.apiKey && autoIdentity.accessToken) {
494
+ const functionsBaseUrl = getFunctionsBaseUrl(resolved.supabaseUrl);
495
+ fetch(`${functionsBaseUrl}/external-sso-launch`, {
496
+ method: "POST",
497
+ headers: {
498
+ "Content-Type": "application/json",
499
+ Authorization: `Bearer ${autoIdentity.accessToken}`
500
+ },
501
+ body: JSON.stringify({
502
+ apiKey: config.apiKey,
503
+ nextPath: threadPath
504
+ })
505
+ }).then((res) => res.ok ? res.json() : null).then((data) => {
506
+ if (data?.redirect_url) {
507
+ window.open(data.redirect_url, "_blank", "noopener,noreferrer");
508
+ } else {
509
+ window.open(`${baseUrl}${threadPath}`, "_blank", "noopener,noreferrer");
510
+ }
511
+ }).catch(() => {
512
+ window.open(`${baseUrl}${threadPath}`, "_blank", "noopener,noreferrer");
513
+ });
514
+ return;
515
+ }
516
+ window.open(`${baseUrl}${threadPath}`, "_blank", "noopener,noreferrer");
517
+ }, [resolved.supabaseUrl, resolved.siteUrl, config.apiKey, autoIdentity.accessToken]);
490
518
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
491
519
  FeedbackProviderInner,
492
520
  {
@@ -513,6 +541,7 @@ function FeedbackProvider({
513
541
  userId: effectiveUserId,
514
542
  userEmail: effectiveEmail,
515
543
  userDisplayName: effectiveDisplayName,
544
+ openThreadInDashboard,
516
545
  children
517
546
  }
518
547
  );
@@ -541,7 +570,8 @@ function FeedbackProviderInner({
541
570
  debug,
542
571
  userId,
543
572
  userEmail,
544
- userDisplayName
573
+ userDisplayName,
574
+ openThreadInDashboard
545
575
  }) {
546
576
  const debugLog = (0, import_react2.useCallback)((message, extra) => {
547
577
  if (!debug) return;
@@ -932,7 +962,8 @@ function FeedbackProviderInner({
932
962
  setPendingScreenshotPin,
933
963
  submitThread,
934
964
  submitScreenshotThread,
935
- refreshThreads: fetchThreads
965
+ refreshThreads: fetchThreads,
966
+ openThreadInDashboard
936
967
  },
937
968
  children: [
938
969
  children,
@@ -1292,7 +1323,7 @@ function ThreadPins() {
1292
1323
  const signedUrlCache = (0, import_react5.useRef)({});
1293
1324
  const threads = ctx?.threads ?? EMPTY_THREADS;
1294
1325
  const client = ctx?.client;
1295
- const threadBaseUrl = ctx?.siteUrl?.replace(/\/+$/, "") || window.location.origin;
1326
+ const openThreadInDashboard = ctx?.openThreadInDashboard;
1296
1327
  const scrollContainer = ctx?.scrollContainer;
1297
1328
  const container = scrollContainer?.ref.current ?? null;
1298
1329
  const getSignedUrl = (0, import_react5.useCallback)(async (path) => {
@@ -1416,8 +1447,8 @@ function ThreadPins() {
1416
1447
  });
1417
1448
  return;
1418
1449
  }
1419
- window.open(`${threadBaseUrl}/threads/${pin.threadId}`, "_blank", "noopener,noreferrer");
1420
- }, [getSignedUrl, threadBaseUrl]);
1450
+ openThreadInDashboard?.(pin.threadId);
1451
+ }, [getSignedUrl, openThreadInDashboard]);
1421
1452
  const containerLayer = (0, import_react5.useMemo)(() => {
1422
1453
  if (!container || containerPositions.length === 0) return null;
1423
1454
  const { width, height } = getContainerContentSize(container);
@@ -1544,7 +1575,7 @@ function ReviewSurfaceOverlay() {
1544
1575
  const mode = ctx?.mode ?? "view";
1545
1576
  const reviewUrl = ctx?.reviewUrl ?? null;
1546
1577
  const threads = ctx?.threads ?? EMPTY_THREADS2;
1547
- const threadBaseUrl = ctx?.siteUrl?.replace(/\/+$/, "") || window.location.origin;
1578
+ const openThreadInDashboard = ctx?.openThreadInDashboard;
1548
1579
  const updateMetrics = (0, import_react6.useCallback)(() => {
1549
1580
  const iframe = iframeRef.current;
1550
1581
  if (!iframe) return;
@@ -1722,7 +1753,7 @@ function ReviewSurfaceOverlay() {
1722
1753
  index: index + 1,
1723
1754
  left,
1724
1755
  top,
1725
- onClick: () => window.open(`${threadBaseUrl}/threads/${thread.id}`, "_blank", "noopener,noreferrer")
1756
+ onClick: () => openThreadInDashboard?.(thread.id)
1726
1757
  },
1727
1758
  thread.id
1728
1759
  );
@@ -2168,10 +2199,52 @@ function getLauncherCapabilities({
2168
2199
 
2169
2200
  // src/FeedbackButton.tsx
2170
2201
  var import_jsx_runtime7 = require("react/jsx-runtime");
2202
+ var LAUNCHER_POS_KEY = "issue-pin:launcher-pos";
2203
+ var DRAG_THRESHOLD = 5;
2204
+ var EDGE_MARGIN = 20;
2205
+ var BTN_SIZE = 48;
2206
+ var SNAP_EASING = "cubic-bezier(0.2, 0, 0, 1)";
2207
+ var SNAP_TRANSITION = `left 0.3s ${SNAP_EASING}, top 0.3s ${SNAP_EASING}`;
2208
+ function loadPos(initialEdge) {
2209
+ try {
2210
+ const raw = localStorage.getItem(LAUNCHER_POS_KEY);
2211
+ if (raw) {
2212
+ const parsed = JSON.parse(raw);
2213
+ if ((parsed.edge === "left" || parsed.edge === "right") && typeof parsed.y === "number") {
2214
+ return { edge: parsed.edge, y: parsed.y };
2215
+ }
2216
+ }
2217
+ } catch {
2218
+ }
2219
+ return {
2220
+ edge: initialEdge === "bottom-left" ? "left" : "right",
2221
+ y: window.innerHeight - BTN_SIZE - EDGE_MARGIN
2222
+ };
2223
+ }
2224
+ function savePos(pos) {
2225
+ try {
2226
+ localStorage.setItem(LAUNCHER_POS_KEY, JSON.stringify(pos));
2227
+ } catch {
2228
+ }
2229
+ }
2230
+ function clampY(y) {
2231
+ return Math.min(
2232
+ Math.max(y, EDGE_MARGIN),
2233
+ window.innerHeight - BTN_SIZE - EDGE_MARGIN
2234
+ );
2235
+ }
2236
+ function snapXForEdge(edge) {
2237
+ return edge === "left" ? EDGE_MARGIN : window.innerWidth - BTN_SIZE - EDGE_MARGIN;
2238
+ }
2171
2239
  function FeedbackButton({ position = "bottom-right" }) {
2172
2240
  const ctx = useFeedbackSafe();
2173
2241
  const menuRef = (0, import_react9.useRef)(null);
2174
2242
  const [hintDismissed, setHintDismissed] = (0, import_react9.useState)(false);
2243
+ const [pos, setPos] = (0, import_react9.useState)(() => loadPos(position));
2244
+ const [dragXY, setDragXY] = (0, import_react9.useState)(null);
2245
+ const [snapping, setSnapping] = (0, import_react9.useState)(false);
2246
+ const dragStateRef = (0, import_react9.useRef)(null);
2247
+ const snapTimerRef = (0, import_react9.useRef)(null);
2175
2248
  const menuOpenState = ctx?.menuOpen ?? false;
2176
2249
  const debug = ctx?.debug ?? false;
2177
2250
  (0, import_react9.useEffect)(() => {
@@ -2189,6 +2262,79 @@ function FeedbackButton({ position = "bottom-right" }) {
2189
2262
  console.log("[EW SDK] FeedbackButton mounted");
2190
2263
  return () => console.log("[EW SDK] FeedbackButton unmounted");
2191
2264
  }, [debug]);
2265
+ (0, import_react9.useEffect)(() => {
2266
+ const handleResize = () => {
2267
+ setPos((prev) => {
2268
+ const newPos = {
2269
+ edge: prev.edge,
2270
+ y: clampY(prev.y)
2271
+ };
2272
+ savePos(newPos);
2273
+ return newPos;
2274
+ });
2275
+ };
2276
+ window.addEventListener("resize", handleResize);
2277
+ return () => window.removeEventListener("resize", handleResize);
2278
+ }, []);
2279
+ (0, import_react9.useEffect)(() => {
2280
+ return () => {
2281
+ if (snapTimerRef.current) clearTimeout(snapTimerRef.current);
2282
+ };
2283
+ }, []);
2284
+ const handlePointerDown = (0, import_react9.useCallback)((e) => {
2285
+ if (e.button != null && e.button !== 0) return;
2286
+ e.currentTarget.setPointerCapture(e.pointerId);
2287
+ dragStateRef.current = {
2288
+ active: false,
2289
+ startX: e.clientX,
2290
+ startY: e.clientY,
2291
+ currentX: e.clientX,
2292
+ currentY: e.clientY,
2293
+ didDrag: false
2294
+ };
2295
+ }, []);
2296
+ const handlePointerMove = (0, import_react9.useCallback)((e) => {
2297
+ const ds = dragStateRef.current;
2298
+ if (!ds) return;
2299
+ const dx = e.clientX - ds.startX;
2300
+ const dy = e.clientY - ds.startY;
2301
+ const distance = Math.sqrt(dx * dx + dy * dy);
2302
+ if (!ds.active && distance >= DRAG_THRESHOLD) {
2303
+ ds.active = true;
2304
+ ds.didDrag = true;
2305
+ }
2306
+ if (ds.active) {
2307
+ ds.currentX = e.clientX;
2308
+ ds.currentY = e.clientY;
2309
+ const x = e.clientX - BTN_SIZE / 2;
2310
+ const y = clampY(e.clientY - BTN_SIZE / 2);
2311
+ setDragXY({ x, y });
2312
+ }
2313
+ }, []);
2314
+ const handlePointerUp = (0, import_react9.useCallback)((e) => {
2315
+ try {
2316
+ e.currentTarget.releasePointerCapture(e.pointerId);
2317
+ } catch {
2318
+ }
2319
+ const ds = dragStateRef.current;
2320
+ if (!ds || !ds.didDrag) {
2321
+ dragStateRef.current = null;
2322
+ return;
2323
+ }
2324
+ const centerX = ds.currentX;
2325
+ const edge = centerX < window.innerWidth / 2 ? "left" : "right";
2326
+ const y = clampY(ds.currentY - BTN_SIZE / 2);
2327
+ const newPos = { edge, y };
2328
+ setSnapping(true);
2329
+ setPos(newPos);
2330
+ savePos(newPos);
2331
+ setDragXY(null);
2332
+ if (snapTimerRef.current) clearTimeout(snapTimerRef.current);
2333
+ snapTimerRef.current = setTimeout(() => {
2334
+ setSnapping(false);
2335
+ snapTimerRef.current = null;
2336
+ }, 300);
2337
+ }, []);
2192
2338
  if (!ctx) return null;
2193
2339
  const {
2194
2340
  mode,
@@ -2204,9 +2350,13 @@ function FeedbackButton({ position = "bottom-right" }) {
2204
2350
  } = ctx;
2205
2351
  const annotate = mode === "annotate";
2206
2352
  const capabilities = getLauncherCapabilities({ allowPinOnPage: canPinOnPage, allowScreenshot: canScreenshot });
2207
- const posStyle = position === "bottom-left" ? { left: 20, bottom: 20 } : { right: 20, bottom: 20 };
2208
- const hintStyle = position === "bottom-left" ? { left: 56, bottom: 8 } : { right: 56, bottom: 8 };
2209
2353
  const handleToggle = () => {
2354
+ if (dragStateRef.current?.didDrag) {
2355
+ dragStateRef.current.didDrag = false;
2356
+ dragStateRef.current = null;
2357
+ return;
2358
+ }
2359
+ dragStateRef.current = null;
2210
2360
  if (debug) console.log("[EW SDK] handleToggle", { mode, menuOpen });
2211
2361
  setHintDismissed(true);
2212
2362
  if (annotate) {
@@ -2226,12 +2376,44 @@ function FeedbackButton({ position = "bottom-right" }) {
2226
2376
  const capturing = screenshotCapturing;
2227
2377
  const showLauncherHint = showHints && !hintDismissed && !annotate && !menuOpen;
2228
2378
  if (capabilities.actionCount === 0) return null;
2379
+ const restX = snapXForEdge(pos.edge);
2380
+ let wrapperStyle;
2381
+ if (dragXY !== null) {
2382
+ wrapperStyle = {
2383
+ position: "fixed",
2384
+ left: dragXY.x,
2385
+ top: dragXY.y,
2386
+ transition: "none",
2387
+ zIndex: Z.launcher
2388
+ };
2389
+ } else if (snapping) {
2390
+ wrapperStyle = {
2391
+ position: "fixed",
2392
+ left: restX,
2393
+ top: pos.y,
2394
+ transition: SNAP_TRANSITION,
2395
+ zIndex: Z.launcher
2396
+ };
2397
+ } else {
2398
+ wrapperStyle = {
2399
+ position: "fixed",
2400
+ left: restX,
2401
+ top: pos.y,
2402
+ transition: "none",
2403
+ zIndex: Z.launcher
2404
+ };
2405
+ }
2406
+ const hintStyle = pos.edge === "right" ? { right: BTN_SIZE + 8, top: "50%", transform: "translateY(-50%)" } : { left: BTN_SIZE + 8, top: "50%", transform: "translateY(-50%)" };
2407
+ const menuPositionStyle = pos.edge === "right" ? { right: 0, bottom: BTN_SIZE + 4 } : { left: 0, bottom: BTN_SIZE + 4 };
2229
2408
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2230
2409
  "div",
2231
2410
  {
2232
2411
  ref: menuRef,
2233
2412
  "data-ew-feedback-interactive": "true",
2234
- style: { position: "fixed", ...posStyle, zIndex: Z.launcher },
2413
+ style: { ...wrapperStyle, touchAction: "none" },
2414
+ onPointerDown: handlePointerDown,
2415
+ onPointerMove: handlePointerMove,
2416
+ onPointerUp: handlePointerUp,
2235
2417
  children: [
2236
2418
  showLauncherHint && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2237
2419
  "div",
@@ -2258,8 +2440,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2258
2440
  {
2259
2441
  style: {
2260
2442
  position: "absolute",
2261
- bottom: 56,
2262
- right: 0,
2443
+ ...menuPositionStyle,
2263
2444
  marginBottom: 4,
2264
2445
  width: 176,
2265
2446
  borderRadius: 8,
@@ -2324,7 +2505,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2324
2505
  children: [
2325
2506
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react3.Camera, { style: { width: 16, height: 16, color: T.primary, flexShrink: 0 } }),
2326
2507
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
2327
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontWeight: 500 }, children: capturing ? "Capturing\u2026" : "Screenshot" }),
2508
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontWeight: 500 }, children: capturing ? "Capturing..." : "Screenshot" }),
2328
2509
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: T.muted }, children: "Capture current view" })
2329
2510
  ] })
2330
2511
  ]
@@ -2339,8 +2520,8 @@ function FeedbackButton({ position = "bottom-right" }) {
2339
2520
  onClick: handleToggle,
2340
2521
  style: {
2341
2522
  display: "flex",
2342
- height: 48,
2343
- width: 48,
2523
+ height: BTN_SIZE,
2524
+ width: BTN_SIZE,
2344
2525
  alignItems: "center",
2345
2526
  justifyContent: "center",
2346
2527
  borderRadius: "50%",
@@ -2349,7 +2530,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2349
2530
  border: annotate ? "none" : `1px solid ${T.border}`,
2350
2531
  background: annotate ? T.primary : T.card,
2351
2532
  color: annotate ? T.primaryFg : T.fg,
2352
- cursor: "pointer",
2533
+ cursor: dragXY !== null ? "grabbing" : "pointer",
2353
2534
  transform: annotate ? "scale(1.1)" : "scale(1)"
2354
2535
  },
2355
2536
  title: annotate ? "Exit feedback mode" : "Open feedback menu",