@qaiddev/thumbs-embed 1.3.0 → 1.4.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.
Files changed (39) hide show
  1. package/dist/bootstrap.d.ts +15 -0
  2. package/dist/chunks/annotate-BNibaDg3.js +367 -0
  3. package/dist/chunks/annotate-BNibaDg3.js.map +1 -0
  4. package/dist/chunks/bootstrap-U4yiPm5-.js +1099 -0
  5. package/dist/chunks/bootstrap-U4yiPm5-.js.map +1 -0
  6. package/dist/chunks/modal-BX5wqy4s.js +119 -0
  7. package/dist/chunks/modal-BX5wqy4s.js.map +1 -0
  8. package/dist/chunks/network-capture-DMbMwwwR.js +63 -0
  9. package/dist/chunks/network-capture-DMbMwwwR.js.map +1 -0
  10. package/dist/chunks/recording--IwOtRL2.js +256 -0
  11. package/dist/chunks/recording--IwOtRL2.js.map +1 -0
  12. package/dist/chunks/screenshot-BkOCLuS2.js +33 -0
  13. package/dist/chunks/screenshot-BkOCLuS2.js.map +1 -0
  14. package/dist/chunks/screenshot-dom-CME7tYRE.js +63 -0
  15. package/dist/chunks/screenshot-dom-CME7tYRE.js.map +1 -0
  16. package/dist/chunks/targeting-DwdDr9Cm.js +295 -0
  17. package/dist/chunks/targeting-DwdDr9Cm.js.map +1 -0
  18. package/dist/chunks/video-BOYqf2Im.js +209 -0
  19. package/dist/chunks/video-BOYqf2Im.js.map +1 -0
  20. package/dist/dom.d.ts +36 -0
  21. package/dist/embed.d.ts +53 -68
  22. package/dist/index.d.ts +10 -12
  23. package/dist/index.js +22 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/loader.d.ts +18 -0
  26. package/dist/loader.js +7 -0
  27. package/dist/loader.js.map +1 -0
  28. package/dist/modal.d.ts +49 -0
  29. package/dist/qaid.umd.cjs +23 -23
  30. package/dist/qaid.umd.cjs.map +1 -1
  31. package/dist/recording.d.ts +74 -0
  32. package/dist/targeting.d.ts +72 -0
  33. package/dist/types.d.ts +7 -1
  34. package/dist/video-capture.d.ts +7 -0
  35. package/dist/video-orientation.d.ts +2 -0
  36. package/dist/video-redaction.d.ts +58 -0
  37. package/package.json +9 -5
  38. package/dist/qaid.js +0 -2013
  39. package/dist/qaid.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-BOYqf2Im.js","sources":["../../src/video-orientation.ts","../../src/video-redaction.ts","../../src/video-capture.ts"],"sourcesContent":["/**\n * iOS/iPadOS screen-recording orientation fix.\n *\n * On iOS/iPadOS, `getDisplayMedia` + `MediaRecorder` writes a device-orientation\n * rotation into the recorded file (even for screen captures). Players disagree\n * on whether to honour it — the widget's own preview `<video>` renders it\n * sideways, and so does the dashboard. Re-encoding the *live* decoded frames\n * (which carry no container rotation) through a canvas produces a file whose\n * pixels are already upright with no rotation metadata, so it renders the same\n * everywhere.\n *\n * If the live frames themselves come back with swapped dimensions (portrait\n * frame while the viewport is landscape, or vice versa), we additionally rotate\n * the canvas 90° to match the viewport. That branch only triggers on a clear\n * orientation mismatch, so a correctly-oriented recording is never rotated.\n */\n\nexport interface OrientationTransform {\n /** Output canvas width in px. */\n width: number;\n /** Output canvas height in px. */\n height: number;\n /** Clockwise rotation to apply while drawing: 0, 90, or -90. */\n rotate: 0 | 90 | -90;\n}\n\nexport interface OrientationCorrectedStream {\n /** Canvas-sourced stream to hand to MediaRecorder. */\n stream: MediaStream;\n /** Stop drawing and release the canvas stream + source video. */\n stop: () => void;\n}\n\n/** True on iPhone/iPad/iPod, including iPadOS 13+ which reports as desktop Mac. */\nexport function isIOSDevice(): boolean {\n const ua = navigator.userAgent || \"\";\n return (\n /iP(hone|ad|od)/.test(ua) ||\n // iPadOS 13+ masquerades as \"MacIntel\" but is a multi-touch device.\n (navigator.platform === \"MacIntel\" && (navigator.maxTouchPoints || 0) > 1)\n );\n}\n\n/**\n * Decide the output canvas size and rotation needed to render a `frameW×frameH`\n * capture upright for a `viewportW×viewportH` viewport. When the frame and\n * viewport share an orientation, no rotation is applied (a straight re-encode,\n * which still strips the bad container rotation). Otherwise rotate 90°, its\n * direction chosen from the screen orientation angle.\n */\nexport function computeOrientationTransform(\n frameW: number,\n frameH: number,\n viewportW: number,\n viewportH: number,\n screenAngle: number\n): OrientationTransform {\n const frameLandscape = frameW >= frameH;\n const viewportLandscape = viewportW >= viewportH;\n\n if (frameLandscape === viewportLandscape) {\n return { width: frameW, height: frameH, rotate: 0 };\n }\n\n // Landscape-secondary (angle 270) is turned the opposite way to primary (90).\n const rotate: 90 | -90 = screenAngle === 270 ? -90 : 90;\n return { width: frameH, height: frameW, rotate };\n}\n\n/** Draw one video frame onto the canvas context, applying the transform. */\nexport function drawRotatedFrame(\n ctx: CanvasRenderingContext2D,\n source: CanvasImageSource,\n t: OrientationTransform\n): void {\n if (t.rotate === 0) {\n ctx.drawImage(source, 0, 0, t.width, t.height);\n return;\n }\n\n ctx.save();\n if (t.rotate === 90) {\n ctx.translate(t.width, 0);\n ctx.rotate(Math.PI / 2);\n } else {\n ctx.translate(0, t.height);\n ctx.rotate(-Math.PI / 2);\n }\n // The source is drawn at its natural (unrotated) extents, which are the\n // output dimensions swapped back.\n ctx.drawImage(source, 0, 0, t.height, t.width);\n ctx.restore();\n}\n\nfunction getScreenAngle(): number {\n const orientation =\n typeof screen !== \"undefined\" ? screen.orientation : undefined;\n return orientation && typeof orientation.angle === \"number\"\n ? orientation.angle\n : 0;\n}\n\nexport function waitForVideoDimensions(video: HTMLVideoElement): Promise<void> {\n return new Promise<void>((resolve) => {\n if (video.videoWidth > 0) {\n resolve();\n return;\n }\n video.addEventListener(\"loadedmetadata\", () => resolve(), { once: true });\n });\n}\n\nexport async function playSilently(video: HTMLVideoElement): Promise<void> {\n try {\n await video.play();\n } catch {\n // Autoplay can reject without a gesture; the element still produces frames\n // for the canvas, so this is non-fatal.\n }\n}\n\n/**\n * Build a canvas-backed, orientation-normalised copy of a display-capture\n * stream. Returns null when the browser can't support the canvas pipeline, in\n * which case the caller should record the original stream directly.\n */\nexport async function buildOrientationCorrectedStream(\n displayStream: MediaStream,\n frameRate = 15\n): Promise<OrientationCorrectedStream | null> {\n const canvas = document.createElement(\"canvas\");\n if (typeof canvas.captureStream !== \"function\") return null;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n const video = document.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n video.srcObject = displayStream;\n\n await waitForVideoDimensions(video);\n\n const transform = computeOrientationTransform(\n video.videoWidth,\n video.videoHeight,\n window.innerWidth,\n window.innerHeight,\n getScreenAngle()\n );\n canvas.width = transform.width;\n canvas.height = transform.height;\n\n await playSilently(video);\n\n let rafId = 0;\n const draw = (): void => {\n drawRotatedFrame(ctx, video, transform);\n rafId = requestAnimationFrame(draw);\n };\n draw();\n\n const stream = canvas.captureStream(frameRate);\n\n return {\n stream,\n stop: (): void => {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = 0;\n stream.getTracks().forEach((t) => t.stop());\n video.pause();\n video.srcObject = null;\n },\n };\n}\n","/**\n * Live video redaction.\n *\n * Blurs the on-screen bounding boxes of chosen elements while the current tab\n * is recorded. Every frame re-reads each element's `getBoundingClientRect()`,\n * so the blur tracks the content as the page scrolls or reflows — no pixel\n * tracking and no post-recording re-encode. The captured frame is the tab's\n * viewport (video-capture restricts capture to the current tab), so viewport\n * CSS coordinates map onto the frame with a single scale factor.\n *\n * The display stream is drawn to a canvas, the tracked regions are blurred on\n * top, and MediaRecorder records the canvas stream instead of the raw capture —\n * mirroring the iOS orientation pipeline in `video-orientation.ts`.\n */\n\nimport { waitForVideoDimensions, playSilently } from \"./video-orientation\";\n\nexport interface RedactedStream {\n /** Canvas-sourced stream to hand to MediaRecorder. */\n stream: MediaStream;\n /** Stop drawing and release the canvas stream + source video. */\n stop: () => void;\n}\n\nexport interface RedactionOptions {\n /** Output frame rate of the redacted stream. Default: 15. */\n frameRate?: number;\n /** Gaussian blur radius in px applied to each region. Default: 12. */\n blurRadius?: number;\n}\n\ninterface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Map a viewport rect (CSS px, from `getBoundingClientRect()`) onto the\n * captured frame, clamped to the frame bounds. Returns null when the rect is\n * fully outside the viewport (the element scrolled away) or has no area.\n */\nexport function mapRectToFrame(\n rect: { left: number; top: number; width: number; height: number },\n scaleX: number,\n scaleY: number,\n frameW: number,\n frameH: number\n): Box | null {\n const x = Math.max(0, Math.min(frameW, rect.left * scaleX));\n const y = Math.max(0, Math.min(frameH, rect.top * scaleY));\n const right = Math.max(0, Math.min(frameW, (rect.left + rect.width) * scaleX));\n const bottom = Math.max(0, Math.min(frameH, (rect.top + rect.height) * scaleY));\n const w = right - x;\n const h = bottom - y;\n if (w <= 0 || h <= 0) return null;\n return { x, y, w, h };\n}\n\n/**\n * Draw one frame: the full capture, then each element's live bounding box\n * blurred on top. Exported for unit testing the per-frame logic without a real\n * capture stream. When the 2D context can't apply a blur filter, the region is\n * filled solid instead so sensitive content is still hidden.\n */\nexport function drawRedactedFrame(\n ctx: CanvasRenderingContext2D,\n video: CanvasImageSource,\n elements: Element[],\n frameW: number,\n frameH: number,\n blurRadius: number,\n supportsFilter: boolean\n): void {\n ctx.drawImage(video, 0, 0, frameW, frameH);\n\n // The current-tab capture is the viewport, so this scale is ~devicePixelRatio;\n // deriving it live also absorbs viewport resizes mid-recording.\n const scaleX = frameW / Math.max(1, window.innerWidth);\n const scaleY = frameH / Math.max(1, window.innerHeight);\n\n for (const el of elements) {\n const box = mapRectToFrame(\n el.getBoundingClientRect(),\n scaleX,\n scaleY,\n frameW,\n frameH\n );\n if (!box) continue; // scrolled out of view\n\n ctx.save();\n if (supportsFilter) {\n ctx.filter = `blur(${blurRadius}px)`;\n // Re-sample just this region from the live frame so the blur source is\n // the region itself.\n ctx.drawImage(video, box.x, box.y, box.w, box.h, box.x, box.y, box.w, box.h);\n } else {\n // No canvas filter support — hide the region with a solid fill.\n ctx.fillStyle = \"#0b0b0b\";\n ctx.fillRect(box.x, box.y, box.w, box.h);\n }\n ctx.restore();\n }\n}\n\n/**\n * Build a canvas-backed copy of a display-capture stream that blurs the live\n * bounding boxes of `elements`. Returns null when the browser can't support the\n * canvas pipeline (no `captureStream` / 2D context), in which case the caller\n * should record the original stream unredacted.\n */\nexport async function buildRedactedStream(\n displayStream: MediaStream,\n elements: Element[],\n options: RedactionOptions = {}\n): Promise<RedactedStream | null> {\n const frameRate = options.frameRate ?? 15;\n const blurRadius = options.blurRadius ?? 12;\n\n const canvas = document.createElement(\"canvas\");\n if (typeof canvas.captureStream !== \"function\") return null;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n // Feature-detect canvas blur; fall back to solid fill where unsupported.\n const supportsFilter = \"filter\" in ctx;\n\n const video = document.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n video.srcObject = displayStream;\n\n await waitForVideoDimensions(video);\n canvas.width = video.videoWidth;\n canvas.height = video.videoHeight;\n\n await playSilently(video);\n\n let rafId = 0;\n const draw = (): void => {\n drawRedactedFrame(\n ctx,\n video,\n elements,\n canvas.width,\n canvas.height,\n blurRadius,\n supportsFilter\n );\n rafId = requestAnimationFrame(draw);\n };\n draw();\n\n const stream = canvas.captureStream(frameRate);\n\n return {\n stream,\n stop: (): void => {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = 0;\n stream.getTracks().forEach((t) => t.stop());\n video.pause();\n video.srcObject = null;\n },\n };\n}\n","/**\n * Video capture utilities\n * Uses getDisplayMedia + MediaRecorder for screen recording\n */\n\nimport {\n isIOSDevice,\n buildOrientationCorrectedStream,\n type OrientationCorrectedStream,\n} from \"./video-orientation\";\nimport { buildRedactedStream, type RedactedStream } from \"./video-redaction\";\n\nexport interface VideoRecorderOptions {\n /** Max recording duration in seconds. Default: 15 */\n maxDuration?: number;\n /** Video bitrate in bps. Default: 800000 (800kbps) */\n videoBitsPerSecond?: number;\n /**\n * Elements whose live bounding boxes are blurred out of the recording. The\n * blur tracks each element as the page scrolls. Empty/omitted = no redaction.\n */\n redactionElements?: Element[];\n /** Blur radius in px for redacted regions. Default: 12. */\n redactionBlurRadius?: number;\n}\n\nexport interface VideoRecorder {\n /** Start recording. Requests getDisplayMedia if not already started. */\n start(): Promise<void>;\n /** Stop recording and return the video blob. */\n stop(): Promise<Blob>;\n /** Register a callback for each second tick (receives seconds elapsed). */\n onTick(callback: (elapsed: number) => void): void;\n /** Register a callback for when recording stops (e.g., browser stop button, max duration). */\n onStop(callback: (blob: Blob | null) => void): void;\n /** Clean up all resources. */\n destroy(): void;\n}\n\n/**\n * Check if video recording is supported in this browser\n */\nexport function isVideoRecordingSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n !!navigator.mediaDevices &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\" &&\n typeof MediaRecorder !== \"undefined\"\n );\n}\n\n/**\n * Detect the best supported MIME type for recording\n */\nexport function getSupportedMimeType(): string {\n if (typeof MediaRecorder === \"undefined\") return \"\";\n\n const candidates = [\n \"video/webm;codecs=vp9\",\n \"video/webm;codecs=vp8\",\n \"video/webm\",\n \"video/mp4\",\n ];\n\n for (const type of candidates) {\n if (MediaRecorder.isTypeSupported(type)) {\n return type;\n }\n }\n\n return \"\";\n}\n\n/**\n * Create a video recorder that captures the screen\n */\nexport function createVideoRecorder(options: VideoRecorderOptions = {}): VideoRecorder {\n const maxDuration = options.maxDuration ?? 15;\n const videoBitsPerSecond = options.videoBitsPerSecond ?? 800_000;\n const redactionElements = options.redactionElements ?? [];\n const redactionBlurRadius = options.redactionBlurRadius;\n\n let stream: MediaStream | null = null;\n let orientationCorrection: OrientationCorrectedStream | null = null;\n let redaction: RedactedStream | null = null;\n let recorder: MediaRecorder | null = null;\n let chunks: Blob[] = [];\n let tickCallback: ((elapsed: number) => void) | null = null;\n let stopCallback: ((blob: Blob | null) => void) | null = null;\n let tickInterval: ReturnType<typeof setInterval> | null = null;\n let startTime = 0;\n let resolveStop: ((blob: Blob) => void) | null = null;\n let rejectStop: ((err: Error) => void) | null = null;\n let stopped = false;\n let autoStopTimeout: ReturnType<typeof setTimeout> | null = null;\n\n function cleanup(): void {\n if (tickInterval !== null) {\n clearInterval(tickInterval);\n tickInterval = null;\n }\n if (autoStopTimeout !== null) {\n clearTimeout(autoStopTimeout);\n autoStopTimeout = null;\n }\n if (orientationCorrection) {\n orientationCorrection.stop();\n orientationCorrection = null;\n }\n if (redaction) {\n redaction.stop();\n redaction = null;\n }\n if (stream) {\n stream.getTracks().forEach((t) => t.stop());\n stream = null;\n }\n recorder = null;\n chunks = [];\n tickCallback = null;\n stopCallback = null;\n resolveStop = null;\n rejectStop = null;\n }\n\n function finishRecording(): void {\n if (stopped) return;\n stopped = true;\n\n if (recorder && recorder.state !== \"inactive\") {\n recorder.stop();\n }\n }\n\n return {\n async start(): Promise<void> {\n stopped = false;\n chunks = [];\n\n const mimeType = getSupportedMimeType();\n if (!mimeType) {\n throw new Error(\"No supported video MIME type found\");\n }\n\n // Request screen capture, restricted to the current tab (the page the\n // embed is running on). `preferCurrentTab` collapses the Chromium picker\n // to a single \"share this tab\" choice; the other hints drop window and\n // whole-screen options where the browser honours them. These keys are\n // not in the DOM types yet.\n stream = await navigator.mediaDevices.getDisplayMedia({\n video: {\n frameRate: 15,\n displaySurface: \"browser\",\n },\n audio: false,\n // @ts-expect-error preferCurrentTab is not in the TS types yet\n preferCurrentTab: true,\n selfBrowserSurface: \"include\",\n monitorTypeSurfaces: \"exclude\",\n surfaceSwitching: \"exclude\",\n });\n\n // Enforce tab-only capture. In browsers that ignore the hints above and\n // still let the user share a window or the whole screen, reject it — qaid\n // records only the tab it is embedded in, never other windows or the\n // desktop.\n const videoTrack = stream.getVideoTracks()[0];\n const surface = videoTrack?.getSettings?.()?.displaySurface;\n if (surface === \"monitor\" || surface === \"window\") {\n stream.getTracks().forEach((t) => t.stop());\n stream = null;\n throw new Error(\n \"qaid records only the current tab — please share this tab, not a window or your whole screen.\",\n );\n }\n\n // Handle browser \"Stop sharing\" button\n if (videoTrack) {\n videoTrack.addEventListener(\"ended\", () => {\n finishRecording();\n });\n }\n\n // Route the raw capture through a canvas pipeline when needed. On\n // iOS/iPadOS the captured file is rotated by the OS, so re-encode the live\n // frames upright. Otherwise, if the user marked regions to redact, blur\n // their live bounding boxes into the recording. Both fall back to the raw\n // stream if the canvas pipeline isn't available.\n let recordStream: MediaStream = stream;\n if (isIOSDevice()) {\n const corrected = await buildOrientationCorrectedStream(stream);\n if (corrected) {\n orientationCorrection = corrected;\n recordStream = corrected.stream;\n }\n } else if (redactionElements.length > 0) {\n const redacted = await buildRedactedStream(stream, redactionElements, {\n blurRadius: redactionBlurRadius,\n });\n if (redacted) {\n redaction = redacted;\n recordStream = redacted.stream;\n }\n }\n\n recorder = new MediaRecorder(recordStream, {\n mimeType,\n videoBitsPerSecond,\n });\n\n recorder.ondataavailable = (e: BlobEvent) => {\n if (e.data.size > 0) {\n chunks.push(e.data);\n }\n };\n\n recorder.onstop = () => {\n const blob = new Blob(chunks, { type: mimeType });\n if (resolveStop) {\n resolveStop(blob);\n }\n // Notify via callback (for unexpected stops like browser stop button)\n if (stopCallback) {\n stopCallback(blob);\n }\n // Stop the canvas pipeline (iOS orientation / redaction) and the\n // display stream tracks.\n if (orientationCorrection) {\n orientationCorrection.stop();\n orientationCorrection = null;\n }\n if (redaction) {\n redaction.stop();\n redaction = null;\n }\n if (stream) {\n stream.getTracks().forEach((t) => t.stop());\n }\n };\n\n recorder.onerror = () => {\n if (rejectStop) {\n rejectStop(new Error(\"MediaRecorder error\"));\n }\n };\n\n recorder.start(1000); // collect data every second\n startTime = Date.now();\n\n // Start tick interval\n tickInterval = setInterval(() => {\n const elapsed = Math.floor((Date.now() - startTime) / 1000);\n if (tickCallback) {\n tickCallback(elapsed);\n }\n }, 1000);\n\n // Auto-stop at max duration\n autoStopTimeout = setTimeout(() => {\n finishRecording();\n }, maxDuration * 1000);\n },\n\n stop(): Promise<Blob> {\n return new Promise<Blob>((resolve, reject) => {\n resolveStop = resolve;\n rejectStop = reject;\n finishRecording();\n });\n },\n\n onTick(callback: (elapsed: number) => void): void {\n tickCallback = callback;\n },\n\n onStop(callback: (blob: Blob | null) => void): void {\n stopCallback = callback;\n },\n\n destroy(): void {\n finishRecording();\n cleanup();\n },\n };\n}\n"],"names":["isIOSDevice","ua","computeOrientationTransform","frameW","frameH","viewportW","viewportH","screenAngle","frameLandscape","viewportLandscape","drawRotatedFrame","ctx","source","t","getScreenAngle","orientation","waitForVideoDimensions","video","resolve","playSilently","buildOrientationCorrectedStream","displayStream","frameRate","canvas","transform","rafId","draw","stream","mapRectToFrame","rect","scaleX","scaleY","x","y","right","bottom","w","h","drawRedactedFrame","elements","blurRadius","supportsFilter","el","box","buildRedactedStream","options","isVideoRecordingSupported","getSupportedMimeType","candidates","type","createVideoRecorder","maxDuration","videoBitsPerSecond","redactionElements","redactionBlurRadius","orientationCorrection","redaction","recorder","chunks","tickCallback","stopCallback","tickInterval","startTime","resolveStop","rejectStop","stopped","autoStopTimeout","cleanup","finishRecording","mimeType","videoTrack","surface","recordStream","corrected","redacted","e","blob","elapsed","reject","callback"],"mappings":"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"}
package/dist/dom.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Nano DOM factory — a purpose-built, construction-only helper for this embed.
3
+ *
4
+ * The embed builds all of its UI imperatively inside a Shadow DOM. That means
5
+ * the same create → set class/style/attrs → wire events → append dance repeats
6
+ * across every button, overlay, modal, and recording control. `h()` collapses
7
+ * that dance into a single declarative expression.
8
+ *
9
+ * It is deliberately NOT a framework: no reactivity, no virtual DOM, no runtime
10
+ * state. It only constructs. That fits what the embed actually needs (one-shot
11
+ * DOM construction plus a few imperative updates), adds no dependency, and
12
+ * keeps the package zero-dependency. Aliasing `document` here also lets the
13
+ * minifier mangle the call site, which the global `document.createElement`
14
+ * can't be.
15
+ */
16
+ export interface ElProps {
17
+ /** className */
18
+ class?: string;
19
+ /** Inline styles as a cssText string. */
20
+ style?: string;
21
+ /** innerHTML — for icons / trusted SVG markup. */
22
+ html?: string;
23
+ /** textContent. */
24
+ text?: string;
25
+ /** Attributes set via setAttribute (aria-*, data-*, type, role, …). */
26
+ attrs?: Record<string, string>;
27
+ /** Event listeners keyed by type. */
28
+ on?: Record<string, (e: Event) => void>;
29
+ }
30
+ type Child = Node | string | null | undefined | false;
31
+ /**
32
+ * Create an element, apply props, and append children. Falsy children are
33
+ * skipped so conditional content reads cleanly (`cond && h(...)`).
34
+ */
35
+ export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, props?: ElProps | null, ...children: Child[]): HTMLElementTagNameMap[K];
36
+ export {};
package/dist/embed.d.ts CHANGED
@@ -15,35 +15,26 @@ export declare class QaidFeedback {
15
15
  private selectedBounds;
16
16
  private feedbackId;
17
17
  private activeQuest;
18
- private mousePos;
19
18
  private isMobile;
20
19
  private visitorId;
21
20
  private consoleCapture;
22
- private videoRecorder;
23
- private networkCapture;
24
- private recordedBlob;
25
- private recordingIndicator;
26
- private videoPreview;
27
- private isRecording;
28
- private isSendingVideo;
21
+ private recording;
22
+ private targeting;
23
+ private targetingPrewarmed;
24
+ private prewarmHandle;
25
+ private prewarmIsTimeout;
26
+ private videoPrewarmed;
27
+ private screenshotPrewarmed;
29
28
  private shadowHost;
30
29
  private shadowRoot;
31
30
  private overlayShadowHost;
32
31
  private overlayShadowRoot;
33
32
  private buttonsContainer;
34
33
  private isUserProvidedContainer;
35
- private overlayContainer;
36
- private captureLayer;
37
- private crosshairH;
38
- private crosshairV;
39
- private scope;
40
- private highlightBox;
41
- private marker;
42
- private modalContainer;
43
- private backdrop;
34
+ private modal;
35
+ private modalPrewarmed;
44
36
  private dismissBtn;
45
37
  private cssVars;
46
- private keyboardController;
47
38
  private activeThumbBtn;
48
39
  private keyboardActivation;
49
40
  private dialogTrigger;
@@ -51,11 +42,6 @@ export declare class QaidFeedback {
51
42
  private dialogRestoreInert;
52
43
  private readonly uid;
53
44
  private boundKeyDown;
54
- private boundMouseMove;
55
- private boundClick;
56
- private boundTouchStart;
57
- private boundTouchEnd;
58
- private touchStartPos;
59
45
  private boundResize;
60
46
  private _startDismissed;
61
47
  private destroyed;
@@ -79,6 +65,16 @@ export declare class QaidFeedback {
79
65
  private closeDialogA11y;
80
66
  private clearActiveThumb;
81
67
  private init;
68
+ /**
69
+ * Best-effort preload of a lazily-split feature chunk so its first use is
70
+ * instant. Warming fetches + compiles (and defines) the module; the feature
71
+ * modules have no load-time side effects, so this is safe. Errors are
72
+ * swallowed — a failed preload just falls back to an on-demand load.
73
+ */
74
+ private prewarmVideo;
75
+ private prewarmScreenshot;
76
+ /** Run fn when the main thread is idle; cancelled by destroy(). */
77
+ private schedulePrewarm;
82
78
  /**
83
79
  * Watch for the shadow hosts being removed from the DOM by framework
84
80
  * client-side navigation (e.g. Astro View Transitions swapping <body>
@@ -106,34 +102,7 @@ export declare class QaidFeedback {
106
102
  private handleDismiss;
107
103
  private handleThumbClick;
108
104
  private submitDirectFeedback;
109
- private startTargeting;
110
- /**
111
- * Keyboard-driven targeting. Mirrors startTargeting minus the mouse
112
- * plumbing: no `qaid-targeting` body class (keeps the cursor visible for
113
- * keyboard users), no mouse reticle, and no document mouse/click listeners.
114
- * The KeyboardTargetingController owns Tab/Arrow/Enter/Space/Escape.
115
- */
116
- private startKeyboardTargetingFlow;
117
- private selectKeyboardTarget;
118
- private createTargetingOverlay;
119
105
  private handleKeyDown;
120
- private handleMouseMove;
121
- /** Move the crosshair/scope reticle and highlight the element under (x, y).
122
- * Shared by the mouse (hover) and touch (drag) targeting paths. */
123
- private updateReticleAt;
124
- private handleClick;
125
- private handleTouchStart;
126
- private handleTouchEnd;
127
- /** Select the element under (x, y) and tear down targeting.
128
- * Shared by the mouse (click) and touch (touchend) targeting paths. */
129
- private selectAt;
130
- /** Remove the targeting overlay and every mouse/touch/keyboard listener the
131
- * pointer-targeting flow attaches to the document. */
132
- private stopTargetingListeners;
133
- private cancelTargeting;
134
- private removeTargetingOverlay;
135
- private showSelectedMarker;
136
- private hideSelectedMarker;
137
106
  /** Whether to capture the screenshot with the DOM/canvas method (html2canvas)
138
107
  * instead of the permission-based Screen Capture API. Explicit "dom" wins;
139
108
  * otherwise DOM is used on touch devices to avoid the getDisplayMedia prompt. */
@@ -166,24 +135,40 @@ export declare class QaidFeedback {
166
135
  * path (which bypasses the modal entirely).
167
136
  */
168
137
  private resetFeedbackUi;
169
- private showModal;
170
- private showBottomSheet;
171
- private showPositionedModal;
172
- private getModalContent;
173
- private setupModalInteractions;
174
- private submitMessage;
175
- private closeModal;
176
- private startRecording;
177
- private stopRecording;
178
- private showRecordingIndicator;
179
- private updateRecordingTimer;
180
- private formatTime;
181
- private removeRecordingIndicator;
182
- private showRecordingPreview;
183
- private cancelRecordingPreview;
184
- private removeVideoPreview;
185
- private submitVideoFeedback;
186
- private cleanupRecording;
138
+ /**
139
+ * Lazily load and instantiate the modal controller. The message modal lives
140
+ * in a separate chunk, fetched the first time it opens (and pre-warmed while
141
+ * the user targets an element — see prewarmModal).
142
+ */
143
+ private ensureModal;
144
+ /** Narrow view of the embed the modal controller talks back through. */
145
+ private makeModalHost;
146
+ /** Warm the modal chunk while the user is targeting, so it opens instantly. */
147
+ private prewarmModal;
148
+ /**
149
+ * Lazily load and instantiate the targeting controller. The subsystem (with
150
+ * element-selector) lives in a separate chunk, fetched the first time the
151
+ * user targets — pre-warmed on thumb-button hover (see prewarmTargeting).
152
+ */
153
+ private ensureTargeting;
154
+ /** Narrow view of the embed the targeting controller talks back through. */
155
+ private makeTargetingHost;
156
+ /** Warm the targeting chunk on thumb-button hover, before the click. */
157
+ private prewarmTargeting;
158
+ /**
159
+ * Cheap synchronous capability check so the record button can render without
160
+ * pulling in the (lazily-loaded) video subsystem. Mirrors
161
+ * isVideoRecordingSupported() in video-capture.ts.
162
+ */
163
+ private videoSupported;
164
+ /**
165
+ * Lazily load and instantiate the recording controller. The whole recording
166
+ * subsystem lives in a separate chunk, fetched only the first time the record
167
+ * button is used (and pre-warmed on hover — see prewarmVideo).
168
+ */
169
+ private ensureRecording;
170
+ /** Narrow view of the embed the recording controller talks back through. */
171
+ private makeRecordingHost;
187
172
  private setButtonsDisabled;
188
173
  /**
189
174
  * Destroy the embed and clean up all resources
package/dist/index.d.ts CHANGED
@@ -7,25 +7,23 @@
7
7
  * ```typescript
8
8
  * import { QaidFeedback } from '@qaiddev/thumbs-embed';
9
9
  *
10
- * const embed = new QaidFeedback({
11
- * endpoint: '/api/feedback'
12
- * });
10
+ * const embed = new QaidFeedback({ endpoint: '/api/feedback' });
13
11
  * ```
14
12
  *
15
- * Usage via script tag:
13
+ * Usage via script tag (full bundle):
16
14
  * ```html
17
- * <script src="feedback.umd.cjs" data-endpoint="/api/feedback"></script>
15
+ * <script src="qaid.umd.cjs" data-endpoint="/api/feedback"></script>
18
16
  * ```
19
17
  *
20
- * Or manually initialize:
18
+ * For progressive loading (small core + on-demand screenshot/video/redaction
19
+ * chunks), load the ESM loader instead:
21
20
  * ```html
22
- * <script src="feedback.umd.cjs"></script>
23
- * <script>
24
- * const embed = new QaidFeedback.QaidFeedback({
25
- * endpoint: '/api/feedback'
26
- * });
27
- * </script>
21
+ * <script type="module" src="…/loader.js" data-endpoint="/api/feedback"></script>
28
22
  * ```
23
+ *
24
+ * This entry re-exports the full public API. Because those re-exports pull the
25
+ * feature modules into this chunk, this entry is the "full" bundle; the
26
+ * `loader` entry stays lean so its features split into on-demand chunks.
29
27
  */
30
28
  import { QaidFeedback } from "./embed";
31
29
  export { QaidFeedback };
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ import { a as o } from "./chunks/bootstrap-U4yiPm5-.js";
2
+ import { Q as a } from "./chunks/bootstrap-U4yiPm5-.js";
3
+ import { c as i } from "./chunks/network-capture-DMbMwwwR.js";
4
+ import { createVideoRecorder as n, getSupportedMimeType as d, isVideoRecordingSupported as c } from "./chunks/video-BOYqf2Im.js";
5
+ import { captureDomScreenshot as f, isDomScreenshotSupported as u } from "./chunks/screenshot-dom-CME7tYRE.js";
6
+ import { A as x, c as A, d as h, o as E, r as g } from "./chunks/annotate-BNibaDg3.js";
7
+ o();
8
+ export {
9
+ x as AnnotationEditor,
10
+ a as QaidFeedback,
11
+ f as captureDomScreenshot,
12
+ i as captureNetworkErrors,
13
+ A as compositeAnnotations,
14
+ n as createVideoRecorder,
15
+ h as drawShape,
16
+ d as getSupportedMimeType,
17
+ u as isDomScreenshotSupported,
18
+ c as isVideoRecordingSupported,
19
+ E as openAnnotationEditor,
20
+ g as rectFromPoints
21
+ };
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @qaiddev/thumbs-embed - Standalone Feedback Embed\n *\n * A zero-dependency feedback embed with element targeting.\n *\n * Usage via ES module:\n * ```typescript\n * import { QaidFeedback } from '@qaiddev/thumbs-embed';\n *\n * const embed = new QaidFeedback({ endpoint: '/api/feedback' });\n * ```\n *\n * Usage via script tag (full bundle):\n * ```html\n * <script src=\"qaid.umd.cjs\" data-endpoint=\"/api/feedback\"></script>\n * ```\n *\n * For progressive loading (small core + on-demand screenshot/video/redaction\n * chunks), load the ESM loader instead:\n * ```html\n * <script type=\"module\" src=\"…/loader.js\" data-endpoint=\"/api/feedback\"></script>\n * ```\n *\n * This entry re-exports the full public API. Because those re-exports pull the\n * feature modules into this chunk, this entry is the \"full\" bundle; the\n * `loader` entry stays lean so its features split into on-demand chunks.\n */\n\nimport { QaidFeedback } from \"./embed\";\nimport { autoInit } from \"./bootstrap\";\n\nexport { QaidFeedback };\nexport type {\n FeedbackConfig,\n QuestsLaunchConfig,\n ResolvedFeedbackConfig,\n ConsoleError,\n NetworkError,\n FeedbackData,\n SelectedBounds,\n FeedbackPayload,\n FeedbackResponse,\n FeedbackMessagePayload,\n} from \"./types\";\nexport { captureNetworkErrors } from \"./network-capture\";\nexport type { NetworkCapture } from \"./network-capture\";\nexport { createVideoRecorder, isVideoRecordingSupported, getSupportedMimeType } from \"./video-capture\";\nexport type { VideoRecorder, VideoRecorderOptions } from \"./video-capture\";\nexport { captureDomScreenshot, isDomScreenshotSupported } from \"./screenshot-dom\";\nexport type { DomScreenshotOptions } from \"./screenshot-dom\";\nexport {\n openAnnotationEditor,\n compositeAnnotations,\n drawShape,\n rectFromPoints,\n AnnotationEditor,\n} from \"./annotate\";\nexport type {\n Shape,\n ShapeType,\n Point,\n AnnotationLabels,\n AnnotationEditorOptions,\n} from \"./annotate\";\n\n// Auto-initialize from a script tag (legacy UMD / full-bundle path).\nautoInit();\n"],"names":["autoInit"],"mappings":";;;;;;AAkEAA,EAAA;"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Minimal ESM loader entry for progressive delivery.
3
+ *
4
+ * ```html
5
+ * <script type="module" src="…/loader.js" data-endpoint="/api/feedback"></script>
6
+ * ```
7
+ *
8
+ * Ships only the core (thumbs buttons, modal, element targeting, submit).
9
+ * Screenshot capture, the annotation editor, and the video subsystem
10
+ * (including redaction) load on demand as separate chunks the first time each
11
+ * is used — so a visitor who only gives a thumbs-up never downloads them.
12
+ *
13
+ * The full programmatic API (createVideoRecorder, openAnnotationEditor, …)
14
+ * lives on the package's main entry (`@qaiddev/thumbs-embed`).
15
+ */
16
+ import { QaidFeedback } from "./embed";
17
+ export { QaidFeedback };
18
+ export type { FeedbackConfig } from "./types";
package/dist/loader.js ADDED
@@ -0,0 +1,7 @@
1
+ import { a } from "./chunks/bootstrap-U4yiPm5-.js";
2
+ import { Q as e } from "./chunks/bootstrap-U4yiPm5-.js";
3
+ a();
4
+ export {
5
+ e as QaidFeedback
6
+ };
7
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sources":["../src/loader.ts"],"sourcesContent":["/**\n * Minimal ESM loader entry for progressive delivery.\n *\n * ```html\n * <script type=\"module\" src=\"…/loader.js\" data-endpoint=\"/api/feedback\"></script>\n * ```\n *\n * Ships only the core (thumbs buttons, modal, element targeting, submit).\n * Screenshot capture, the annotation editor, and the video subsystem\n * (including redaction) load on demand as separate chunks the first time each\n * is used — so a visitor who only gives a thumbs-up never downloads them.\n *\n * The full programmatic API (createVideoRecorder, openAnnotationEditor, …)\n * lives on the package's main entry (`@qaiddev/thumbs-embed`).\n */\n\nimport { QaidFeedback } from \"./embed\";\nimport { autoInit } from \"./bootstrap\";\n\nexport { QaidFeedback };\nexport type { FeedbackConfig } from \"./types\";\n\nautoInit();\n"],"names":["autoInit"],"mappings":";;AAsBAA,EAAA;"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Feedback message modal — extracted from embed.ts into its own lazily-loaded
3
+ * chunk. It's fetched the first time a message modal opens (after feedback is
4
+ * submitted), and pre-warmed while the user is targeting an element.
5
+ *
6
+ * The controller owns the modal/backdrop DOM and the message PATCH flow, and
7
+ * talks back to the embed through the narrow `ModalHost` interface.
8
+ */
9
+ import type { ResolvedFeedbackConfig, EmbedState, FeedbackData, SelectedBounds } from "./types";
10
+ /** The slice of the embed the modal needs. */
11
+ export interface ModalHost {
12
+ readonly config: ResolvedFeedbackConfig;
13
+ readonly uid: string;
14
+ readonly isMobile: boolean;
15
+ readonly state: EmbedState;
16
+ readonly boundKeyDown: (e: KeyboardEvent) => void;
17
+ /** Live references — the toggle mutates feedbackType; positioning reads bounds. */
18
+ readonly feedbackData: FeedbackData;
19
+ readonly selectedBounds: SelectedBounds;
20
+ readonly feedbackId: number | null;
21
+ setFeedbackId(id: number | null): void;
22
+ ensureOverlayHost(): ShadowRoot;
23
+ applyVars(el: HTMLElement): void;
24
+ announceMsg(message: string, assertive?: boolean): void;
25
+ openDialogA11y(container: HTMLElement, opts: {
26
+ labelledbyId?: string;
27
+ describedbyId?: string;
28
+ label?: string;
29
+ }): void;
30
+ closeDialogA11y(): void;
31
+ resetFeedbackUi(): void;
32
+ }
33
+ export declare class ModalController {
34
+ private host;
35
+ private modalContainer;
36
+ private backdrop;
37
+ constructor(host: ModalHost);
38
+ open(): void;
39
+ /** Escape / external close. Runs the finalize-PATCH + teardown. */
40
+ close(): void;
41
+ /** DOM/listener teardown only — no PATCH, no state reset (for embed destroy). */
42
+ destroy(): void;
43
+ private teardown;
44
+ private showBottomSheet;
45
+ private showPositionedModal;
46
+ private getModalContent;
47
+ private setupModalInteractions;
48
+ private submitMessage;
49
+ }