@loupekit/sdk 0.9.3-next.37 → 0.9.3-next.38

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.js CHANGED
@@ -308,6 +308,14 @@ var STYLES = (
308
308
  page stays visible and tappable; the list returns when the tool closes. */
309
309
  .dock.inspecting { height: auto !important; }
310
310
  .dock.inspecting .listhead, .dock.inspecting .list { display: none; }
311
+ /* The composer is a bottom sheet on a phone: full width, thumb-reachable, and it
312
+ scrolls when the on-screen keyboard is up. JS sets its position inline (hence the
313
+ !important) \u2014 on small screens the sheet wins. */
314
+ .composer {
315
+ top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
316
+ width: auto !important; max-height: 80vh; overflow-y: auto;
317
+ border-radius: 16px 16px 0 0; padding-bottom: 16px;
318
+ }
311
319
  }
312
320
  `
313
321
  );
@@ -2431,6 +2439,26 @@ function hash(s) {
2431
2439
  for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
2432
2440
  return h;
2433
2441
  }
2442
+ function isTouchDevice() {
2443
+ try {
2444
+ return typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
2445
+ } catch {
2446
+ return false;
2447
+ }
2448
+ }
2449
+ function dataUrlToFile(dataUrl, name) {
2450
+ const m = /^data:([^;,]+)(;base64)?,([\s\S]*)$/.exec(dataUrl);
2451
+ if (!m) return null;
2452
+ const [, type, b64, payload = ""] = m;
2453
+ try {
2454
+ const raw = b64 ? atob(payload) : decodeURIComponent(payload);
2455
+ const bytes = new Uint8Array(raw.length);
2456
+ for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
2457
+ return new File([bytes], name, { type: type || "image/png" });
2458
+ } catch {
2459
+ return null;
2460
+ }
2461
+ }
2434
2462
  var LoupeApp = class {
2435
2463
  constructor(cfg) {
2436
2464
  this.comments = [];
@@ -2799,7 +2827,7 @@ var LoupeApp = class {
2799
2827
  const recordBtn = this.toolBtn(RECORD_ICON, "Record", "record");
2800
2828
  recordBtn.title = "Drag a box, record a screen video of it, and comment";
2801
2829
  recordBtn.onclick = () => this.setMode(this.mode === "record" ? "off" : "record");
2802
- const canRecord = typeof navigator.mediaDevices?.getDisplayMedia === "function";
2830
+ const canRecord = !isTouchDevice() && typeof navigator.mediaDevices?.getDisplayMedia === "function";
2803
2831
  tools.append(inspectBtn, freeBtn, regionBtn, ...canRecord ? [recordBtn] : []);
2804
2832
  const listHead = el("div", "listhead");
2805
2833
  listHead.append(document.createTextNode("Comments"));
@@ -2947,6 +2975,34 @@ var LoupeApp = class {
2947
2975
  const region = { x: vp.x + window.scrollX, y: vp.y + window.scrollY, w: vp.w, h: vp.h, rel };
2948
2976
  return { region, element: centerEl };
2949
2977
  }
2978
+ /**
2979
+ * Touch path for the Region tool: capture what is on screen right now and open the
2980
+ * composer with it already attached. The reporter scrolls to the part of the page they
2981
+ * mean FIRST, then taps Region — no drag, so nothing fights the page scroll.
2982
+ */
2983
+ async captureViewportForComposer() {
2984
+ const vp = { x: 0, y: 0, w: window.innerWidth, h: window.innerHeight };
2985
+ const capture = this.cfg.captureRegion ?? captureRegionScreenshot;
2986
+ let shot;
2987
+ try {
2988
+ shot = await capture(vp);
2989
+ } catch {
2990
+ shot = void 0;
2991
+ }
2992
+ const file = shot ? dataUrlToFile(shot, "screenshot.png") : null;
2993
+ const docX = window.scrollX + vp.w / 2;
2994
+ const docY = window.scrollY + vp.h / 2;
2995
+ const docW = Math.max(1, document.documentElement.scrollWidth);
2996
+ const docH = Math.max(1, document.documentElement.scrollHeight);
2997
+ const offset = { x: clamp(docX / docW), y: clamp(docY / docH) };
2998
+ this.setMode("off");
2999
+ this.openComposer(
3000
+ { kind: "free", offset, point: { x: docX, y: docY }, label: "Screenshot \xB7 attached" },
3001
+ 8,
3002
+ window.innerHeight / 2,
3003
+ file ? [file] : []
3004
+ );
3005
+ }
2950
3006
  /** Capture the selected viewport rect, then open the composer for a region comment. */
2951
3007
  async finishRegion(vp) {
2952
3008
  this.selbox.style.display = "none";
@@ -3030,7 +3086,6 @@ var LoupeApp = class {
3030
3086
  document.removeEventListener("click", this.onClick, true);
3031
3087
  document.removeEventListener("click", this.onFreeClick, true);
3032
3088
  document.removeEventListener("pointerdown", this.onRegionDown, true);
3033
- document.documentElement.style.touchAction = mode === "region" || mode === "record" ? "none" : "";
3034
3089
  if (mode === "off") return;
3035
3090
  document.addEventListener("keydown", this.onKey, true);
3036
3091
  this.closeComposer();
@@ -3040,12 +3095,14 @@ var LoupeApp = class {
3040
3095
  document.addEventListener("click", this.onClick, true);
3041
3096
  } else if (mode === "free") {
3042
3097
  document.addEventListener("click", this.onFreeClick, true);
3098
+ } else if (mode === "region" && isTouchDevice()) {
3099
+ void this.captureViewportForComposer();
3043
3100
  } else if (mode === "region" || mode === "record") {
3044
3101
  document.addEventListener("pointerdown", this.onRegionDown, true);
3045
3102
  }
3046
3103
  }
3047
3104
  // ---- composer -------------------------------------------------------------
3048
- openComposer(target, x, y) {
3105
+ openComposer(target, x, y, seedFiles = []) {
3049
3106
  this.pending = target;
3050
3107
  const isRecording = target.kind === "region" && !!target.recording;
3051
3108
  const c = this.composer;
@@ -3053,14 +3110,14 @@ var LoupeApp = class {
3053
3110
  const label = el(
3054
3111
  "div",
3055
3112
  "target",
3056
- target.kind === "element" ? describe(target.element) : target.kind === "region" ? isRecording ? `\u23FA Recording \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : `Region \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : "Free note \xB7 anywhere on the page"
3113
+ target.kind === "element" ? describe(target.element) : target.kind === "region" ? isRecording ? `\u23FA Recording \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : `Region \xB7 ${Math.round(target.region.w)}\xD7${Math.round(target.region.h)} px` : target.label ?? "Free note \xB7 anywhere on the page"
3057
3114
  );
3058
3115
  const title = el("input", "title");
3059
3116
  title.type = "text";
3060
3117
  title.placeholder = "Title \u2014 one line: what's wrong, or what you need";
3061
3118
  const ta = el("textarea");
3062
3119
  ta.placeholder = isRecording ? "Describe the issue in this recording\u2026" : target.kind === "region" ? "Describe the issue in this area\u2026" : target.kind === "free" ? "Describe this note\u2026" : "Describe what should change here\u2026";
3063
- const files = [];
3120
+ const files = seedFiles.slice();
3064
3121
  const attach = el("div", "attach");
3065
3122
  const pick = el("button", "pick", "\uFF0B Attach images / videos");
3066
3123
  const input = document.createElement("input");
@@ -3102,6 +3159,7 @@ var LoupeApp = class {
3102
3159
  };
3103
3160
  pick.onclick = () => input.click();
3104
3161
  attach.append(pick, input, chips, err);
3162
+ if (files.length) drawChips();
3105
3163
  const row = el("div", "row");
3106
3164
  let box = null;
3107
3165
  if (target.kind !== "free" && !isRecording) {
@@ -3133,7 +3191,7 @@ var LoupeApp = class {
3133
3191
  const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
3134
3192
  const top = Math.min(Math.max(8, y + 12), window.innerHeight - h - 8);
3135
3193
  Object.assign(c.style, { display: "block", left: left + "px", top: top + "px" });
3136
- ta.focus();
3194
+ if (!isTouchDevice()) ta.focus();
3137
3195
  }
3138
3196
  closeComposer() {
3139
3197
  this.composer.style.display = "none";