@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.
@@ -335,6 +335,14 @@ var Loupe = (() => {
335
335
  page stays visible and tappable; the list returns when the tool closes. */
336
336
  .dock.inspecting { height: auto !important; }
337
337
  .dock.inspecting .listhead, .dock.inspecting .list { display: none; }
338
+ /* The composer is a bottom sheet on a phone: full width, thumb-reachable, and it
339
+ scrolls when the on-screen keyboard is up. JS sets its position inline (hence the
340
+ !important) \u2014 on small screens the sheet wins. */
341
+ .composer {
342
+ top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
343
+ width: auto !important; max-height: 80vh; overflow-y: auto;
344
+ border-radius: 16px 16px 0 0; padding-bottom: 16px;
345
+ }
338
346
  }
339
347
  `
340
348
  );
@@ -2458,6 +2466,26 @@ var Loupe = (() => {
2458
2466
  for (let i = 0; i < s.length; i++) h = h * 31 + s.charCodeAt(i) | 0;
2459
2467
  return h;
2460
2468
  }
2469
+ function isTouchDevice() {
2470
+ try {
2471
+ return typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
2472
+ } catch {
2473
+ return false;
2474
+ }
2475
+ }
2476
+ function dataUrlToFile(dataUrl, name) {
2477
+ const m = /^data:([^;,]+)(;base64)?,([\s\S]*)$/.exec(dataUrl);
2478
+ if (!m) return null;
2479
+ const [, type, b64, payload = ""] = m;
2480
+ try {
2481
+ const raw = b64 ? atob(payload) : decodeURIComponent(payload);
2482
+ const bytes = new Uint8Array(raw.length);
2483
+ for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
2484
+ return new File([bytes], name, { type: type || "image/png" });
2485
+ } catch {
2486
+ return null;
2487
+ }
2488
+ }
2461
2489
  var LoupeApp = class {
2462
2490
  constructor(cfg) {
2463
2491
  this.comments = [];
@@ -2826,7 +2854,7 @@ var Loupe = (() => {
2826
2854
  const recordBtn = this.toolBtn(RECORD_ICON, "Record", "record");
2827
2855
  recordBtn.title = "Drag a box, record a screen video of it, and comment";
2828
2856
  recordBtn.onclick = () => this.setMode(this.mode === "record" ? "off" : "record");
2829
- const canRecord = typeof navigator.mediaDevices?.getDisplayMedia === "function";
2857
+ const canRecord = !isTouchDevice() && typeof navigator.mediaDevices?.getDisplayMedia === "function";
2830
2858
  tools.append(inspectBtn, freeBtn, regionBtn, ...canRecord ? [recordBtn] : []);
2831
2859
  const listHead = el("div", "listhead");
2832
2860
  listHead.append(document.createTextNode("Comments"));
@@ -2974,6 +3002,34 @@ var Loupe = (() => {
2974
3002
  const region = { x: vp.x + window.scrollX, y: vp.y + window.scrollY, w: vp.w, h: vp.h, rel };
2975
3003
  return { region, element: centerEl };
2976
3004
  }
3005
+ /**
3006
+ * Touch path for the Region tool: capture what is on screen right now and open the
3007
+ * composer with it already attached. The reporter scrolls to the part of the page they
3008
+ * mean FIRST, then taps Region — no drag, so nothing fights the page scroll.
3009
+ */
3010
+ async captureViewportForComposer() {
3011
+ const vp = { x: 0, y: 0, w: window.innerWidth, h: window.innerHeight };
3012
+ const capture = this.cfg.captureRegion ?? captureRegionScreenshot;
3013
+ let shot;
3014
+ try {
3015
+ shot = await capture(vp);
3016
+ } catch {
3017
+ shot = void 0;
3018
+ }
3019
+ const file = shot ? dataUrlToFile(shot, "screenshot.png") : null;
3020
+ const docX = window.scrollX + vp.w / 2;
3021
+ const docY = window.scrollY + vp.h / 2;
3022
+ const docW = Math.max(1, document.documentElement.scrollWidth);
3023
+ const docH = Math.max(1, document.documentElement.scrollHeight);
3024
+ const offset = { x: clamp(docX / docW), y: clamp(docY / docH) };
3025
+ this.setMode("off");
3026
+ this.openComposer(
3027
+ { kind: "free", offset, point: { x: docX, y: docY }, label: "Screenshot \xB7 attached" },
3028
+ 8,
3029
+ window.innerHeight / 2,
3030
+ file ? [file] : []
3031
+ );
3032
+ }
2977
3033
  /** Capture the selected viewport rect, then open the composer for a region comment. */
2978
3034
  async finishRegion(vp) {
2979
3035
  this.selbox.style.display = "none";
@@ -3057,7 +3113,6 @@ var Loupe = (() => {
3057
3113
  document.removeEventListener("click", this.onClick, true);
3058
3114
  document.removeEventListener("click", this.onFreeClick, true);
3059
3115
  document.removeEventListener("pointerdown", this.onRegionDown, true);
3060
- document.documentElement.style.touchAction = mode === "region" || mode === "record" ? "none" : "";
3061
3116
  if (mode === "off") return;
3062
3117
  document.addEventListener("keydown", this.onKey, true);
3063
3118
  this.closeComposer();
@@ -3067,12 +3122,14 @@ var Loupe = (() => {
3067
3122
  document.addEventListener("click", this.onClick, true);
3068
3123
  } else if (mode === "free") {
3069
3124
  document.addEventListener("click", this.onFreeClick, true);
3125
+ } else if (mode === "region" && isTouchDevice()) {
3126
+ void this.captureViewportForComposer();
3070
3127
  } else if (mode === "region" || mode === "record") {
3071
3128
  document.addEventListener("pointerdown", this.onRegionDown, true);
3072
3129
  }
3073
3130
  }
3074
3131
  // ---- composer -------------------------------------------------------------
3075
- openComposer(target, x, y) {
3132
+ openComposer(target, x, y, seedFiles = []) {
3076
3133
  this.pending = target;
3077
3134
  const isRecording = target.kind === "region" && !!target.recording;
3078
3135
  const c = this.composer;
@@ -3080,14 +3137,14 @@ var Loupe = (() => {
3080
3137
  const label = el(
3081
3138
  "div",
3082
3139
  "target",
3083
- 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"
3140
+ 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"
3084
3141
  );
3085
3142
  const title = el("input", "title");
3086
3143
  title.type = "text";
3087
3144
  title.placeholder = "Title \u2014 one line: what's wrong, or what you need";
3088
3145
  const ta = el("textarea");
3089
3146
  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";
3090
- const files = [];
3147
+ const files = seedFiles.slice();
3091
3148
  const attach = el("div", "attach");
3092
3149
  const pick = el("button", "pick", "\uFF0B Attach images / videos");
3093
3150
  const input = document.createElement("input");
@@ -3129,6 +3186,7 @@ var Loupe = (() => {
3129
3186
  };
3130
3187
  pick.onclick = () => input.click();
3131
3188
  attach.append(pick, input, chips, err);
3189
+ if (files.length) drawChips();
3132
3190
  const row = el("div", "row");
3133
3191
  let box = null;
3134
3192
  if (target.kind !== "free" && !isRecording) {
@@ -3160,7 +3218,7 @@ var Loupe = (() => {
3160
3218
  const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
3161
3219
  const top = Math.min(Math.max(8, y + 12), window.innerHeight - h - 8);
3162
3220
  Object.assign(c.style, { display: "block", left: left + "px", top: top + "px" });
3163
- ta.focus();
3221
+ if (!isTouchDevice()) ta.focus();
3164
3222
  }
3165
3223
  closeComposer() {
3166
3224
  this.composer.style.display = "none";