@loupekit/sdk 0.9.3 → 0.10.0-next.40

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 = [];
@@ -2824,7 +2852,7 @@ var Loupe = (() => {
2824
2852
  regionBtn.title = "Drag a free-size box, screenshot it, and comment";
2825
2853
  regionBtn.onclick = () => this.setMode(this.mode === "region" ? "off" : "region");
2826
2854
  const recordBtn = this.toolBtn(RECORD_ICON, "Record", "record");
2827
- recordBtn.title = "Drag a box, record a screen video of it, and comment";
2855
+ recordBtn.title = isTouchDevice() ? "Record your screen, then describe the issue" : "Drag a box, record a screen video of it, and comment";
2828
2856
  recordBtn.onclick = () => this.setMode(this.mode === "record" ? "off" : "record");
2829
2857
  const canRecord = typeof navigator.mediaDevices?.getDisplayMedia === "function";
2830
2858
  tools.append(inspectBtn, freeBtn, regionBtn, ...canRecord ? [recordBtn] : []);
@@ -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,18 @@ 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();
3127
+ } else if (mode === "record" && isTouchDevice()) {
3128
+ const vp = { x: 0, y: 0, w: window.innerWidth, h: window.innerHeight };
3129
+ this.setMode("off");
3130
+ void this.finishRecording(vp);
3070
3131
  } else if (mode === "region" || mode === "record") {
3071
3132
  document.addEventListener("pointerdown", this.onRegionDown, true);
3072
3133
  }
3073
3134
  }
3074
3135
  // ---- composer -------------------------------------------------------------
3075
- openComposer(target, x, y) {
3136
+ openComposer(target, x, y, seedFiles = []) {
3076
3137
  this.pending = target;
3077
3138
  const isRecording = target.kind === "region" && !!target.recording;
3078
3139
  const c = this.composer;
@@ -3080,14 +3141,14 @@ var Loupe = (() => {
3080
3141
  const label = el(
3081
3142
  "div",
3082
3143
  "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"
3144
+ 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
3145
  );
3085
3146
  const title = el("input", "title");
3086
3147
  title.type = "text";
3087
3148
  title.placeholder = "Title \u2014 one line: what's wrong, or what you need";
3088
3149
  const ta = el("textarea");
3089
3150
  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 = [];
3151
+ const files = seedFiles.slice();
3091
3152
  const attach = el("div", "attach");
3092
3153
  const pick = el("button", "pick", "\uFF0B Attach images / videos");
3093
3154
  const input = document.createElement("input");
@@ -3129,6 +3190,7 @@ var Loupe = (() => {
3129
3190
  };
3130
3191
  pick.onclick = () => input.click();
3131
3192
  attach.append(pick, input, chips, err);
3193
+ if (files.length) drawChips();
3132
3194
  const row = el("div", "row");
3133
3195
  let box = null;
3134
3196
  if (target.kind !== "free" && !isRecording) {
@@ -3160,7 +3222,7 @@ var Loupe = (() => {
3160
3222
  const left = Math.min(Math.max(8, x + 12), window.innerWidth - w - 8);
3161
3223
  const top = Math.min(Math.max(8, y + 12), window.innerHeight - h - 8);
3162
3224
  Object.assign(c.style, { display: "block", left: left + "px", top: top + "px" });
3163
- ta.focus();
3225
+ if (!isTouchDevice()) ta.focus();
3164
3226
  }
3165
3227
  closeComposer() {
3166
3228
  this.composer.style.display = "none";