@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 +22 -0
- package/dist/index.cjs +197 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +200 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
1420
|
-
}, [getSignedUrl,
|
|
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
|
|
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: () =>
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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:
|
|
2343
|
-
width:
|
|
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",
|