@sylergydigital/issue-pin-sdk 0.6.4 → 0.6.5

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,23 @@
2
2
 
3
3
  All notable changes to `@sylergydigital/issue-pin-sdk` are documented here.
4
4
 
5
+ ## [0.6.5] - 2026-04-15
6
+
7
+ ### Added
8
+ - **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.
9
+ - Position persistence — launcher position (edge + Y offset) is saved to `localStorage` and restored across page reloads.
10
+ - Menu and hint toast auto-flip to the opposite side of the docked edge so they never render off-screen.
11
+ - Viewport resize clamping — button repositions to stay within bounds when the window is resized.
12
+ - Click vs. drag disambiguation — short interactions (< 5px movement) still toggle the menu normally.
13
+
14
+ ## [0.6.4] - 2026-04-14
15
+
16
+ ### Fixed
17
+ - Included `CHANGELOG.md` in the SDK npm publish pipeline (`files` array in package.json).
18
+
19
+ ### Added
20
+ - SDK CHANGELOG.md with full release history.
21
+
5
22
  ## [0.6.3] - 2026-04-14
6
23
 
7
24
  ### Added
package/dist/index.cjs CHANGED
@@ -2168,10 +2168,52 @@ function getLauncherCapabilities({
2168
2168
 
2169
2169
  // src/FeedbackButton.tsx
2170
2170
  var import_jsx_runtime7 = require("react/jsx-runtime");
2171
+ var LAUNCHER_POS_KEY = "issue-pin:launcher-pos";
2172
+ var DRAG_THRESHOLD = 5;
2173
+ var EDGE_MARGIN = 20;
2174
+ var BTN_SIZE = 48;
2175
+ var SNAP_EASING = "cubic-bezier(0.2, 0, 0, 1)";
2176
+ var SNAP_TRANSITION = `left 0.3s ${SNAP_EASING}, top 0.3s ${SNAP_EASING}`;
2177
+ function loadPos(initialEdge) {
2178
+ try {
2179
+ const raw = localStorage.getItem(LAUNCHER_POS_KEY);
2180
+ if (raw) {
2181
+ const parsed = JSON.parse(raw);
2182
+ if ((parsed.edge === "left" || parsed.edge === "right") && typeof parsed.y === "number") {
2183
+ return { edge: parsed.edge, y: parsed.y };
2184
+ }
2185
+ }
2186
+ } catch {
2187
+ }
2188
+ return {
2189
+ edge: initialEdge === "bottom-left" ? "left" : "right",
2190
+ y: window.innerHeight - BTN_SIZE - EDGE_MARGIN
2191
+ };
2192
+ }
2193
+ function savePos(pos) {
2194
+ try {
2195
+ localStorage.setItem(LAUNCHER_POS_KEY, JSON.stringify(pos));
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ function clampY(y) {
2200
+ return Math.min(
2201
+ Math.max(y, EDGE_MARGIN),
2202
+ window.innerHeight - BTN_SIZE - EDGE_MARGIN
2203
+ );
2204
+ }
2205
+ function snapXForEdge(edge) {
2206
+ return edge === "left" ? EDGE_MARGIN : window.innerWidth - BTN_SIZE - EDGE_MARGIN;
2207
+ }
2171
2208
  function FeedbackButton({ position = "bottom-right" }) {
2172
2209
  const ctx = useFeedbackSafe();
2173
2210
  const menuRef = (0, import_react9.useRef)(null);
2174
2211
  const [hintDismissed, setHintDismissed] = (0, import_react9.useState)(false);
2212
+ const [pos, setPos] = (0, import_react9.useState)(() => loadPos(position));
2213
+ const [dragXY, setDragXY] = (0, import_react9.useState)(null);
2214
+ const [snapping, setSnapping] = (0, import_react9.useState)(false);
2215
+ const dragStateRef = (0, import_react9.useRef)(null);
2216
+ const snapTimerRef = (0, import_react9.useRef)(null);
2175
2217
  const menuOpenState = ctx?.menuOpen ?? false;
2176
2218
  const debug = ctx?.debug ?? false;
2177
2219
  (0, import_react9.useEffect)(() => {
@@ -2189,6 +2231,79 @@ function FeedbackButton({ position = "bottom-right" }) {
2189
2231
  console.log("[EW SDK] FeedbackButton mounted");
2190
2232
  return () => console.log("[EW SDK] FeedbackButton unmounted");
2191
2233
  }, [debug]);
2234
+ (0, import_react9.useEffect)(() => {
2235
+ const handleResize = () => {
2236
+ setPos((prev) => {
2237
+ const newPos = {
2238
+ edge: prev.edge,
2239
+ y: clampY(prev.y)
2240
+ };
2241
+ savePos(newPos);
2242
+ return newPos;
2243
+ });
2244
+ };
2245
+ window.addEventListener("resize", handleResize);
2246
+ return () => window.removeEventListener("resize", handleResize);
2247
+ }, []);
2248
+ (0, import_react9.useEffect)(() => {
2249
+ return () => {
2250
+ if (snapTimerRef.current) clearTimeout(snapTimerRef.current);
2251
+ };
2252
+ }, []);
2253
+ const handlePointerDown = (0, import_react9.useCallback)((e) => {
2254
+ if (e.button != null && e.button !== 0) return;
2255
+ e.currentTarget.setPointerCapture(e.pointerId);
2256
+ dragStateRef.current = {
2257
+ active: false,
2258
+ startX: e.clientX,
2259
+ startY: e.clientY,
2260
+ currentX: e.clientX,
2261
+ currentY: e.clientY,
2262
+ didDrag: false
2263
+ };
2264
+ }, []);
2265
+ const handlePointerMove = (0, import_react9.useCallback)((e) => {
2266
+ const ds = dragStateRef.current;
2267
+ if (!ds) return;
2268
+ const dx = e.clientX - ds.startX;
2269
+ const dy = e.clientY - ds.startY;
2270
+ const distance = Math.sqrt(dx * dx + dy * dy);
2271
+ if (!ds.active && distance >= DRAG_THRESHOLD) {
2272
+ ds.active = true;
2273
+ ds.didDrag = true;
2274
+ }
2275
+ if (ds.active) {
2276
+ ds.currentX = e.clientX;
2277
+ ds.currentY = e.clientY;
2278
+ const x = e.clientX - BTN_SIZE / 2;
2279
+ const y = clampY(e.clientY - BTN_SIZE / 2);
2280
+ setDragXY({ x, y });
2281
+ }
2282
+ }, []);
2283
+ const handlePointerUp = (0, import_react9.useCallback)((e) => {
2284
+ try {
2285
+ e.currentTarget.releasePointerCapture(e.pointerId);
2286
+ } catch {
2287
+ }
2288
+ const ds = dragStateRef.current;
2289
+ if (!ds || !ds.didDrag) {
2290
+ dragStateRef.current = null;
2291
+ return;
2292
+ }
2293
+ const centerX = ds.currentX;
2294
+ const edge = centerX < window.innerWidth / 2 ? "left" : "right";
2295
+ const y = clampY(ds.currentY - BTN_SIZE / 2);
2296
+ const newPos = { edge, y };
2297
+ setSnapping(true);
2298
+ setPos(newPos);
2299
+ savePos(newPos);
2300
+ setDragXY(null);
2301
+ if (snapTimerRef.current) clearTimeout(snapTimerRef.current);
2302
+ snapTimerRef.current = setTimeout(() => {
2303
+ setSnapping(false);
2304
+ snapTimerRef.current = null;
2305
+ }, 300);
2306
+ }, []);
2192
2307
  if (!ctx) return null;
2193
2308
  const {
2194
2309
  mode,
@@ -2204,9 +2319,13 @@ function FeedbackButton({ position = "bottom-right" }) {
2204
2319
  } = ctx;
2205
2320
  const annotate = mode === "annotate";
2206
2321
  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
2322
  const handleToggle = () => {
2323
+ if (dragStateRef.current?.didDrag) {
2324
+ dragStateRef.current.didDrag = false;
2325
+ dragStateRef.current = null;
2326
+ return;
2327
+ }
2328
+ dragStateRef.current = null;
2210
2329
  if (debug) console.log("[EW SDK] handleToggle", { mode, menuOpen });
2211
2330
  setHintDismissed(true);
2212
2331
  if (annotate) {
@@ -2226,12 +2345,44 @@ function FeedbackButton({ position = "bottom-right" }) {
2226
2345
  const capturing = screenshotCapturing;
2227
2346
  const showLauncherHint = showHints && !hintDismissed && !annotate && !menuOpen;
2228
2347
  if (capabilities.actionCount === 0) return null;
2348
+ const restX = snapXForEdge(pos.edge);
2349
+ let wrapperStyle;
2350
+ if (dragXY !== null) {
2351
+ wrapperStyle = {
2352
+ position: "fixed",
2353
+ left: dragXY.x,
2354
+ top: dragXY.y,
2355
+ transition: "none",
2356
+ zIndex: Z.launcher
2357
+ };
2358
+ } else if (snapping) {
2359
+ wrapperStyle = {
2360
+ position: "fixed",
2361
+ left: restX,
2362
+ top: pos.y,
2363
+ transition: SNAP_TRANSITION,
2364
+ zIndex: Z.launcher
2365
+ };
2366
+ } else {
2367
+ wrapperStyle = {
2368
+ position: "fixed",
2369
+ left: restX,
2370
+ top: pos.y,
2371
+ transition: "none",
2372
+ zIndex: Z.launcher
2373
+ };
2374
+ }
2375
+ const hintStyle = pos.edge === "right" ? { right: BTN_SIZE + 8, top: "50%", transform: "translateY(-50%)" } : { left: BTN_SIZE + 8, top: "50%", transform: "translateY(-50%)" };
2376
+ const menuPositionStyle = pos.edge === "right" ? { right: 0, bottom: BTN_SIZE + 4 } : { left: 0, bottom: BTN_SIZE + 4 };
2229
2377
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2230
2378
  "div",
2231
2379
  {
2232
2380
  ref: menuRef,
2233
2381
  "data-ew-feedback-interactive": "true",
2234
- style: { position: "fixed", ...posStyle, zIndex: Z.launcher },
2382
+ style: { ...wrapperStyle, touchAction: "none" },
2383
+ onPointerDown: handlePointerDown,
2384
+ onPointerMove: handlePointerMove,
2385
+ onPointerUp: handlePointerUp,
2235
2386
  children: [
2236
2387
  showLauncherHint && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2237
2388
  "div",
@@ -2258,8 +2409,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2258
2409
  {
2259
2410
  style: {
2260
2411
  position: "absolute",
2261
- bottom: 56,
2262
- right: 0,
2412
+ ...menuPositionStyle,
2263
2413
  marginBottom: 4,
2264
2414
  width: 176,
2265
2415
  borderRadius: 8,
@@ -2324,7 +2474,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2324
2474
  children: [
2325
2475
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_lucide_react3.Camera, { style: { width: 16, height: 16, color: T.primary, flexShrink: 0 } }),
2326
2476
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
2327
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontWeight: 500 }, children: capturing ? "Capturing\u2026" : "Screenshot" }),
2477
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontWeight: 500 }, children: capturing ? "Capturing..." : "Screenshot" }),
2328
2478
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 10, color: T.muted }, children: "Capture current view" })
2329
2479
  ] })
2330
2480
  ]
@@ -2339,8 +2489,8 @@ function FeedbackButton({ position = "bottom-right" }) {
2339
2489
  onClick: handleToggle,
2340
2490
  style: {
2341
2491
  display: "flex",
2342
- height: 48,
2343
- width: 48,
2492
+ height: BTN_SIZE,
2493
+ width: BTN_SIZE,
2344
2494
  alignItems: "center",
2345
2495
  justifyContent: "center",
2346
2496
  borderRadius: "50%",
@@ -2349,7 +2499,7 @@ function FeedbackButton({ position = "bottom-right" }) {
2349
2499
  border: annotate ? "none" : `1px solid ${T.border}`,
2350
2500
  background: annotate ? T.primary : T.card,
2351
2501
  color: annotate ? T.primaryFg : T.fg,
2352
- cursor: "pointer",
2502
+ cursor: dragXY !== null ? "grabbing" : "pointer",
2353
2503
  transform: annotate ? "scale(1.1)" : "scale(1)"
2354
2504
  },
2355
2505
  title: annotate ? "Exit feedback mode" : "Open feedback menu",