@guidelytech/guide-runtime 0.11.0 → 0.12.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.
@@ -57,6 +57,14 @@ interface IdeasApi {
57
57
  ideaPortalLink?(): Promise<string>;
58
58
  }
59
59
 
60
+ /** Where the visitor left the launcher. `y`: 0 = the default bottom spot, 1 = as high as it goes. */
61
+ interface LauncherSpot {
62
+ side: "left" | "right";
63
+ y: number;
64
+ }
65
+ /** Reads a stored spot, ignoring anything malformed. */
66
+ declare function parseLauncherSpot(raw: unknown): LauncherSpot | null;
67
+
60
68
  /** Callbacks the transport invokes while a message is sent and Scout replies. */
61
69
  interface MessengerStreamHandlers {
62
70
  /** A new conversation was created (first message only). */
@@ -159,6 +167,10 @@ interface MessengerOptions {
159
167
  /** The visitor saw these announcements (News tab opened). */
160
168
  onNewsSeen?: (ids: string[]) => void;
161
169
  hideLauncher?: boolean;
170
+ /** Where the visitor last dragged the launcher on this device (null = the configured corner). */
171
+ launcherSpot?: LauncherSpot | null;
172
+ /** The visitor dragged the launcher somewhere else (save it for next time). */
173
+ onLauncherMove?: (spot: LauncherSpot) => void;
162
174
  /** Poll intervals (ms): open thread, and unread badge while closed. */
163
175
  pollMs?: {
164
176
  thread: number;
@@ -188,4 +200,4 @@ declare function relativeTime(iso: string, now?: number): string;
188
200
  /** Intercom-style messenger: launcher + panel with Home, Messages (chat with Scout / the team) and Help tabs. */
189
201
  declare function renderMessenger(options: MessengerOptions): MessengerWidget;
190
202
 
191
- export { type IdeaInput, type IdeaListQuery, type IdeaVoteInput, type IdeasApi, type MessengerApi, type MessengerChecklistProgress, type MessengerNewsItem, type MessengerOptions, type MessengerResumeGuide, type MessengerStreamHandlers, type MessengerTab, type MessengerVisitor, type MessengerWidget, personalGreeting, relativeTime, renderMessenger };
203
+ export { type IdeaInput, type IdeaListQuery, type IdeaVoteInput, type IdeasApi, type LauncherSpot, type MessengerApi, type MessengerChecklistProgress, type MessengerNewsItem, type MessengerOptions, type MessengerResumeGuide, type MessengerStreamHandlers, type MessengerTab, type MessengerVisitor, type MessengerWidget, parseLauncherSpot, personalGreeting, relativeTime, renderMessenger };
@@ -57,6 +57,14 @@ interface IdeasApi {
57
57
  ideaPortalLink?(): Promise<string>;
58
58
  }
59
59
 
60
+ /** Where the visitor left the launcher. `y`: 0 = the default bottom spot, 1 = as high as it goes. */
61
+ interface LauncherSpot {
62
+ side: "left" | "right";
63
+ y: number;
64
+ }
65
+ /** Reads a stored spot, ignoring anything malformed. */
66
+ declare function parseLauncherSpot(raw: unknown): LauncherSpot | null;
67
+
60
68
  /** Callbacks the transport invokes while a message is sent and Scout replies. */
61
69
  interface MessengerStreamHandlers {
62
70
  /** A new conversation was created (first message only). */
@@ -159,6 +167,10 @@ interface MessengerOptions {
159
167
  /** The visitor saw these announcements (News tab opened). */
160
168
  onNewsSeen?: (ids: string[]) => void;
161
169
  hideLauncher?: boolean;
170
+ /** Where the visitor last dragged the launcher on this device (null = the configured corner). */
171
+ launcherSpot?: LauncherSpot | null;
172
+ /** The visitor dragged the launcher somewhere else (save it for next time). */
173
+ onLauncherMove?: (spot: LauncherSpot) => void;
162
174
  /** Poll intervals (ms): open thread, and unread badge while closed. */
163
175
  pollMs?: {
164
176
  thread: number;
@@ -188,4 +200,4 @@ declare function relativeTime(iso: string, now?: number): string;
188
200
  /** Intercom-style messenger: launcher + panel with Home, Messages (chat with Scout / the team) and Help tabs. */
189
201
  declare function renderMessenger(options: MessengerOptions): MessengerWidget;
190
202
 
191
- export { type IdeaInput, type IdeaListQuery, type IdeaVoteInput, type IdeasApi, type MessengerApi, type MessengerChecklistProgress, type MessengerNewsItem, type MessengerOptions, type MessengerResumeGuide, type MessengerStreamHandlers, type MessengerTab, type MessengerVisitor, type MessengerWidget, personalGreeting, relativeTime, renderMessenger };
203
+ export { type IdeaInput, type IdeaListQuery, type IdeaVoteInput, type IdeasApi, type LauncherSpot, type MessengerApi, type MessengerChecklistProgress, type MessengerNewsItem, type MessengerOptions, type MessengerResumeGuide, type MessengerStreamHandlers, type MessengerTab, type MessengerVisitor, type MessengerWidget, parseLauncherSpot, personalGreeting, relativeTime, renderMessenger };
package/dist/messenger.js CHANGED
@@ -1247,6 +1247,107 @@ function createIdeas(o) {
1247
1247
  };
1248
1248
  }
1249
1249
 
1250
+ // src/launcher-drag.ts
1251
+ var DRAG_THRESHOLD = 6;
1252
+ var MARGIN = 16;
1253
+ function parseLauncherSpot(raw) {
1254
+ let v = raw;
1255
+ if (typeof v === "string") {
1256
+ try {
1257
+ v = JSON.parse(v);
1258
+ } catch {
1259
+ return null;
1260
+ }
1261
+ }
1262
+ if (!v || typeof v !== "object") return null;
1263
+ const { side, y } = v;
1264
+ if (side !== "left" && side !== "right" || typeof y !== "number" || !Number.isFinite(y)) return null;
1265
+ return { side, y: Math.min(1, Math.max(0, y)) };
1266
+ }
1267
+ function maxLauncherBottom(viewportHeight, size) {
1268
+ return Math.max(0, viewportHeight - size - MARGIN * 2);
1269
+ }
1270
+ function snapLauncher(drop, viewport, size) {
1271
+ const side = drop.left + size / 2 < viewport.width / 2 ? "left" : "right";
1272
+ const max = maxLauncherBottom(viewport.height, size);
1273
+ const bottom = Math.min(max, Math.max(0, viewport.height - MARGIN - size - drop.top));
1274
+ return { side, y: max ? bottom / max : 0 };
1275
+ }
1276
+ function enableLauncherDrag({ launcher, onMove }) {
1277
+ const win = launcher.ownerDocument.defaultView ?? window;
1278
+ let start = null;
1279
+ let dragging = false;
1280
+ let swallowClickUntil = 0;
1281
+ const onDown = (e) => {
1282
+ if (e.button !== 0) return;
1283
+ const r = launcher.getBoundingClientRect();
1284
+ start = { id: e.pointerId, x: e.clientX, y: e.clientY, left: r.left, top: r.top };
1285
+ dragging = false;
1286
+ try {
1287
+ launcher.setPointerCapture(e.pointerId);
1288
+ } catch {
1289
+ }
1290
+ };
1291
+ const onMovePointer = (e) => {
1292
+ if (!start || e.pointerId !== start.id) return;
1293
+ const dx = e.clientX - start.x;
1294
+ const dy = e.clientY - start.y;
1295
+ if (!dragging) {
1296
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
1297
+ dragging = true;
1298
+ launcher.classList.add("g-m-dragging");
1299
+ }
1300
+ e.preventDefault();
1301
+ const size = launcher.offsetWidth || 56;
1302
+ const left = Math.min(win.innerWidth - size - MARGIN, Math.max(MARGIN, start.left + dx));
1303
+ const top = Math.min(win.innerHeight - size - MARGIN, Math.max(MARGIN, start.top + dy));
1304
+ Object.assign(launcher.style, { left: `${left}px`, top: `${top}px`, right: "auto", bottom: "auto", margin: "0" });
1305
+ };
1306
+ const onUp = (e) => {
1307
+ if (!start || e.pointerId !== start.id) return;
1308
+ start = null;
1309
+ if (!dragging) return;
1310
+ dragging = false;
1311
+ swallowClickUntil = Date.now() + 400;
1312
+ const before = launcher.getBoundingClientRect();
1313
+ launcher.classList.remove("g-m-dragging");
1314
+ Object.assign(launcher.style, { left: "", top: "", right: "", margin: "" });
1315
+ onMove(snapLauncher({ left: before.left, top: before.top }, { width: win.innerWidth, height: win.innerHeight }, before.width || 56));
1316
+ const after = launcher.getBoundingClientRect();
1317
+ const dx = before.left - after.left;
1318
+ const dy = before.top - after.top;
1319
+ if (!dx && !dy) return;
1320
+ launcher.style.transition = "none";
1321
+ launcher.style.transform = `translate(${dx}px, ${dy}px)`;
1322
+ void launcher.offsetWidth;
1323
+ launcher.style.transition = "";
1324
+ launcher.classList.add("g-m-snapping");
1325
+ launcher.style.transform = "";
1326
+ const done = () => launcher.classList.remove("g-m-snapping");
1327
+ launcher.addEventListener("transitionend", done, { once: true });
1328
+ win.setTimeout(done, 400);
1329
+ };
1330
+ const onClick = (e) => {
1331
+ if (Date.now() < swallowClickUntil) {
1332
+ swallowClickUntil = 0;
1333
+ e.preventDefault();
1334
+ e.stopImmediatePropagation();
1335
+ }
1336
+ };
1337
+ launcher.addEventListener("pointerdown", onDown);
1338
+ launcher.addEventListener("pointermove", onMovePointer);
1339
+ launcher.addEventListener("pointerup", onUp);
1340
+ launcher.addEventListener("pointercancel", onUp);
1341
+ launcher.addEventListener("click", onClick, true);
1342
+ return () => {
1343
+ launcher.removeEventListener("pointerdown", onDown);
1344
+ launcher.removeEventListener("pointermove", onMovePointer);
1345
+ launcher.removeEventListener("pointerup", onUp);
1346
+ launcher.removeEventListener("pointercancel", onUp);
1347
+ launcher.removeEventListener("click", onClick, true);
1348
+ };
1349
+ }
1350
+
1250
1351
  // src/messenger-styles.ts
1251
1352
  var MESSENGER_STYLES = `
1252
1353
  :host([data-g-messenger="bottom-right"]) .g-launcher.g-pos-bottom-right{right:72px}
@@ -1255,9 +1356,13 @@ var MESSENGER_STYLES = `
1255
1356
  .g-m-launcher:hover{transform:scale(1.05)}
1256
1357
  .g-m-launcher:focus-visible{outline:2px solid var(--g-primary);outline-offset:3px}
1257
1358
  .g-m-launcher svg{width:26px;height:26px;fill:currentColor}
1359
+ .g-m-launcher{touch-action:none;-webkit-user-select:none;user-select:none}
1360
+ .g-m-launcher.g-m-dragging{transition:none;cursor:grabbing;transform:scale(1.08)}
1361
+ .g-m-launcher.g-m-snapping{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
1258
1362
  .g-m-badge{position:absolute;top:-2px;right:-2px;min-width:20px;height:20px;padding:0 5px;border-radius:999px;background:#e11d48;color:#fff;font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;border:2px solid var(--g-bg)}
1259
- .g-messenger{bottom:76px;height:min(640px,calc(100vh - 110px));transition:width .2s ease,height .2s ease,max-height .2s ease}
1260
- .g-messenger.g-m-expanded{width:min(720px,calc(100vw - 32px));height:calc(100vh - 110px);max-height:calc(100vh - 110px)}
1363
+ .g-messenger{bottom:var(--g-m-bottom,76px);height:min(640px,calc(100vh - var(--g-m-reserve,110px)));transition:width .2s ease,height .2s ease,max-height .2s ease}
1364
+ .g-messenger.g-m-expanded{width:min(720px,calc(100vw - 32px));height:calc(100vh - var(--g-m-reserve,110px));max-height:calc(100vh - var(--g-m-reserve,110px))}
1365
+ .g-messenger.g-m-below{bottom:auto;top:var(--g-m-top)}
1261
1366
  .g-m-expanded .g-rc-article,.g-m-expanded .g-m-scroll{padding:24px clamp(20px,5vw,40px)}
1262
1367
  .g-m-expanded .g-rc-article>.g-title{font-size:24px;line-height:1.25;margin-bottom:12px}
1263
1368
  .g-m-expanded .g-rc-article .g-body{font-size:15px}
@@ -1309,7 +1414,7 @@ var MESSENGER_STYLES = `
1309
1414
  .g-m-typing span{width:6px;height:6px;border-radius:999px;background:var(--g-muted);animation:g-m-bounce 1s infinite}
1310
1415
  .g-m-typing span:nth-child(2){animation-delay:.15s}.g-m-typing span:nth-child(3){animation-delay:.3s}
1311
1416
  @keyframes g-m-bounce{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}
1312
- @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher{transition:none}}
1417
+ @media (prefers-reduced-motion: reduce){.g-m-typing span{animation:none}.g-m-launcher,.g-m-launcher.g-m-snapping{transition:none}}
1313
1418
  .g-m-chips{display:flex;flex-direction:column;gap:6px;margin-top:2px}
1314
1419
  .g-m-chip{display:flex;align-items:center;gap:8px;text-align:left;border:1px solid var(--g-border);background:var(--g-bg);color:var(--g-text);border-radius:10px;padding:8px 10px;font:inherit;font-size:13px;cursor:pointer}
1315
1420
  .g-m-chip:hover{background:var(--g-subtle)}
@@ -1658,7 +1763,6 @@ function renderMessenger(options) {
1658
1763
  const pos = `g-pos-${config.launcher.position}`;
1659
1764
  const rootNode = scope.getRootNode();
1660
1765
  const shadowHost = "host" in rootNode ? rootNode.host : null;
1661
- shadowHost?.setAttribute("data-g-messenger", config.launcher.position);
1662
1766
  const root = el(doc, "div", "g-messenger-root");
1663
1767
  const launcher = el(doc, "button", `g-m-launcher ${pos}`);
1664
1768
  launcher.type = "button";
@@ -1670,6 +1774,47 @@ function renderMessenger(options) {
1670
1774
  launcher.appendChild(badge);
1671
1775
  if (options.hideLauncher) launcher.hidden = true;
1672
1776
  const panel = el(doc, "div", `g-panel g-messenger ${pos}`);
1777
+ let spot = options.launcherSpot ?? null;
1778
+ const win = doc.defaultView ?? window;
1779
+ function placeLauncher() {
1780
+ const side = spot?.side ?? (config.launcher.position === "bottom-left" ? "left" : "right");
1781
+ for (const node of [launcher, panel]) {
1782
+ node.classList.toggle("g-pos-bottom-left", side === "left");
1783
+ node.classList.toggle("g-pos-bottom-right", side === "right");
1784
+ }
1785
+ const size = launcher.offsetWidth || 56;
1786
+ const bottom = spot && !options.hideLauncher ? Math.round(spot.y * maxLauncherBottom(win.innerHeight, size)) : 0;
1787
+ launcher.style.bottom = bottom ? `${bottom}px` : "";
1788
+ if (bottom < size + 16) shadowHost?.setAttribute("data-g-messenger", `bottom-${side}`);
1789
+ else shadowHost?.removeAttribute("data-g-messenger");
1790
+ const launcherTop = win.innerHeight - 16 - size - bottom;
1791
+ const below = bottom > 0 && launcherTop < 480 && bottom > launcherTop;
1792
+ panel.classList.toggle("g-m-below", below);
1793
+ if (!bottom) {
1794
+ panel.style.removeProperty("--g-m-bottom");
1795
+ panel.style.removeProperty("--g-m-top");
1796
+ panel.style.removeProperty("--g-m-reserve");
1797
+ } else if (below) {
1798
+ const top = launcherTop + size - 12;
1799
+ panel.style.setProperty("--g-m-top", `${top}px`);
1800
+ panel.style.setProperty("--g-m-reserve", `${top + 40}px`);
1801
+ } else {
1802
+ panel.style.setProperty("--g-m-bottom", `${bottom + 76}px`);
1803
+ panel.style.setProperty("--g-m-reserve", `${bottom + 110}px`);
1804
+ }
1805
+ }
1806
+ const stopDrag = enableLauncherDrag({
1807
+ launcher,
1808
+ onMove(next) {
1809
+ spot = next;
1810
+ placeLauncher();
1811
+ options.onLauncherMove?.(next);
1812
+ }
1813
+ });
1814
+ const onResize = () => {
1815
+ if (spot) placeLauncher();
1816
+ };
1817
+ win.addEventListener("resize", onResize);
1673
1818
  panel.setAttribute("role", "dialog");
1674
1819
  panel.setAttribute("aria-label", config.name);
1675
1820
  panel.hidden = true;
@@ -1680,6 +1825,7 @@ function renderMessenger(options) {
1680
1825
  panel.append(head, main, tabs);
1681
1826
  root.append(launcher, panel);
1682
1827
  scope.appendChild(root);
1828
+ placeLauncher();
1683
1829
  function withRcText(items, rc, apply) {
1684
1830
  if (!rc?.length) return items;
1685
1831
  const byId = new Map(rc.map((x) => [x.id, x]));
@@ -2766,6 +2912,8 @@ function renderMessenger(options) {
2766
2912
  if (threadTimer) clearTimeout(threadTimer);
2767
2913
  if (backgroundTimer) clearInterval(backgroundTimer);
2768
2914
  panel.removeEventListener("keydown", onKey);
2915
+ stopDrag();
2916
+ win.removeEventListener("resize", onResize);
2769
2917
  help.destroy();
2770
2918
  ideas.destroy();
2771
2919
  shadowHost?.removeAttribute("data-g-messenger");
@@ -2774,6 +2922,7 @@ function renderMessenger(options) {
2774
2922
  };
2775
2923
  }
2776
2924
  export {
2925
+ parseLauncherSpot,
2777
2926
  personalGreeting,
2778
2927
  relativeTime,
2779
2928
  renderMessenger