qapture2 0.2.1 → 0.2.3

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/bin/init.cjs CHANGED
@@ -819,10 +819,10 @@ function genConfigText(opts) {
819
819
  const hintsComment = frameworkHints.length > 0 ? ` *
820
820
  * Auto-detected stack:
821
821
  ${frameworkHints.map((h) => ` * \u2022 ${h}`).join("\n")}` : "";
822
- const typeImport = isTypeScript ? `import type { QaConfig } from 'qapture';
822
+ const typeImport = isTypeScript ? `import type { QaConfig } from 'qapture2';
823
823
 
824
824
  ` : `// @ts-check
825
- /** @type {import('qapture').QaConfig} */
825
+ /** @type {import('qapture2').QaConfig} */
826
826
  `;
827
827
  const typeAnnotation = isTypeScript ? ": QaConfig" : "";
828
828
  const exportStatement = `export default config;
@@ -1073,7 +1073,7 @@ ${DIVIDER}
1073
1073
  Mount the widget near your app root:
1074
1074
  ${DIVIDER}
1075
1075
 
1076
- import { Qapture } from 'qapture';
1076
+ import { Qapture } from 'qapture2';
1077
1077
  import config from './${configFile.replace(/\.[jt]s$/, "")}';
1078
1078
 
1079
1079
  // Render once near your app root:
@@ -1,4 +1,4 @@
1
- import React, { createContext, useEffect, Component, useState, useCallback, useReducer, useContext, useRef, useLayoutEffect } from 'react';
1
+ import React, { createContext, useEffect, Component, useState, useCallback, useRef, useReducer, useContext, useLayoutEffect } from 'react';
2
2
  import ReactDOM from 'react-dom/client';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -1563,28 +1563,165 @@ function Icon({
1563
1563
  function renderSvgElement(tag, attrs, key) {
1564
1564
  return React.createElement(tag, { key, ...attrs });
1565
1565
  }
1566
+ function isCoarsePointer() {
1567
+ if (typeof window === "undefined") return false;
1568
+ try {
1569
+ if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
1570
+ } catch {
1571
+ }
1572
+ return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
1573
+ }
1574
+ function useCoarsePointer() {
1575
+ const [coarse, setCoarse] = useState(() => isCoarsePointer());
1576
+ useEffect(() => {
1577
+ if (typeof window === "undefined" || !window.matchMedia) return;
1578
+ const mq = window.matchMedia("(pointer: coarse)");
1579
+ const on = () => setCoarse(isCoarsePointer());
1580
+ if (mq.addEventListener) mq.addEventListener("change", on);
1581
+ else if (mq.addListener) mq.addListener(on);
1582
+ return () => {
1583
+ if (mq.removeEventListener) mq.removeEventListener("change", on);
1584
+ else if (mq.removeListener) mq.removeListener(on);
1585
+ };
1586
+ }, []);
1587
+ return coarse;
1588
+ }
1589
+ var DEFAULT_LEFT = "calc(1.25rem + env(safe-area-inset-left))";
1590
+ var DEFAULT_BOTTOM = "calc(5rem + env(safe-area-inset-bottom))";
1591
+ var FAB_SIZE_PX = 56;
1592
+ var EDGE_MARGIN = 12;
1593
+ var DRAG_THRESHOLD = 8;
1594
+ var FAB_POS_KEY = "qapture:fabpos";
1595
+ function isFabPos(v) {
1596
+ if (!v || typeof v !== "object") return false;
1597
+ const o = v;
1598
+ return typeof o.left === "number" && Number.isFinite(o.left) && typeof o.bottom === "number" && Number.isFinite(o.bottom);
1599
+ }
1600
+ function loadFabPos() {
1601
+ if (typeof window === "undefined") return null;
1602
+ try {
1603
+ const raw = window.localStorage.getItem(FAB_POS_KEY);
1604
+ if (!raw) return null;
1605
+ const parsed = JSON.parse(raw);
1606
+ return isFabPos(parsed) ? parsed : null;
1607
+ } catch {
1608
+ return null;
1609
+ }
1610
+ }
1611
+ function saveFabPos(pos) {
1612
+ if (typeof window === "undefined") return;
1613
+ try {
1614
+ window.localStorage.setItem(FAB_POS_KEY, JSON.stringify(pos));
1615
+ } catch {
1616
+ }
1617
+ }
1618
+ function clampNum(v, lo, hi) {
1619
+ return Math.min(Math.max(v, lo), hi);
1620
+ }
1621
+ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
1622
+ if (typeof window === "undefined") return p;
1623
+ const maxLeft = Math.max(EDGE_MARGIN, window.innerWidth - w - EDGE_MARGIN);
1624
+ const maxBottom = Math.max(EDGE_MARGIN, window.innerHeight - h - EDGE_MARGIN);
1625
+ return {
1626
+ left: clampNum(p.left, EDGE_MARGIN, maxLeft),
1627
+ bottom: clampNum(p.bottom, EDGE_MARGIN, maxBottom)
1628
+ };
1629
+ }
1566
1630
  function QaFab() {
1567
1631
  const { isOpen, setIsOpen, notes, captureActive, theme } = useQa();
1632
+ const coarse = useCoarsePointer();
1633
+ const [pos, setPos] = useState(() => loadFabPos());
1634
+ const dragRef = useRef(null);
1635
+ const didDragRef = useRef(false);
1568
1636
  if (captureActive) return null;
1637
+ const onPointerDown = (e) => {
1638
+ if (dragRef.current) return;
1639
+ didDragRef.current = false;
1640
+ const target = e.currentTarget;
1641
+ const rect = target.getBoundingClientRect();
1642
+ dragRef.current = {
1643
+ pointerId: e.pointerId,
1644
+ startX: e.clientX,
1645
+ startY: e.clientY,
1646
+ startLeft: rect.left,
1647
+ startBottom: window.innerHeight - rect.bottom,
1648
+ width: rect.width,
1649
+ height: rect.height,
1650
+ dragging: false
1651
+ };
1652
+ try {
1653
+ target.setPointerCapture(e.pointerId);
1654
+ } catch {
1655
+ }
1656
+ };
1657
+ const onPointerMove = (e) => {
1658
+ const d = dragRef.current;
1659
+ if (!d || d.pointerId !== e.pointerId) return;
1660
+ const dx = e.clientX - d.startX;
1661
+ const dy = e.clientY - d.startY;
1662
+ if (!d.dragging) {
1663
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
1664
+ d.dragging = true;
1665
+ }
1666
+ setPos(clampFabPos({ left: d.startLeft + dx, bottom: d.startBottom - dy }, d.width, d.height));
1667
+ };
1668
+ const endDrag = (e) => {
1669
+ const d = dragRef.current;
1670
+ if (!d || d.pointerId !== e.pointerId) return null;
1671
+ try {
1672
+ e.currentTarget.releasePointerCapture(e.pointerId);
1673
+ } catch {
1674
+ }
1675
+ dragRef.current = null;
1676
+ return d;
1677
+ };
1678
+ const onPointerUp = (e) => {
1679
+ const d = endDrag(e);
1680
+ if (!d) return;
1681
+ if (d.dragging) {
1682
+ const dx = e.clientX - d.startX;
1683
+ const dy = e.clientY - d.startY;
1684
+ const next = clampFabPos({ left: d.startLeft + dx, bottom: d.startBottom - dy }, d.width, d.height);
1685
+ setPos(next);
1686
+ saveFabPos(next);
1687
+ didDragRef.current = true;
1688
+ }
1689
+ };
1690
+ const onPointerCancel = (e) => {
1691
+ endDrag(e);
1692
+ };
1693
+ const handleClick = () => {
1694
+ if (didDragRef.current) {
1695
+ didDragRef.current = false;
1696
+ return;
1697
+ }
1698
+ setIsOpen(!isOpen);
1699
+ };
1700
+ const applied = coarse && pos ? clampFabPos(pos) : null;
1701
+ const fabStyle = {
1702
+ left: applied ? `${applied.left}px` : DEFAULT_LEFT,
1703
+ bottom: applied ? `${applied.bottom}px` : DEFAULT_BOTTOM,
1704
+ width: "3.5rem",
1705
+ height: "3.5rem",
1706
+ backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})`,
1707
+ boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04), 0 0 0 2px rgba(255,255,255,0.7)",
1708
+ zIndex: 9990
1709
+ };
1569
1710
  return /* @__PURE__ */ jsxs(
1570
1711
  "button",
1571
1712
  {
1572
1713
  type: "button",
1573
1714
  "data-qa-overlay": "true",
1574
1715
  dir: "ltr",
1575
- onClick: () => setIsOpen(!isOpen),
1716
+ onClick: handleClick,
1717
+ onPointerDown: coarse ? onPointerDown : void 0,
1718
+ onPointerMove: coarse ? onPointerMove : void 0,
1719
+ onPointerUp: coarse ? onPointerUp : void 0,
1720
+ onPointerCancel: coarse ? onPointerCancel : void 0,
1576
1721
  "aria-label": "Qapture \u2014 testing notes",
1577
1722
  title: "Qapture",
1578
- className: "qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-white qa-print-hidden qa-fab-btn",
1579
- style: {
1580
- left: "calc(1.25rem + env(safe-area-inset-left))",
1581
- bottom: "calc(5rem + env(safe-area-inset-bottom))",
1582
- width: "3.5rem",
1583
- height: "3.5rem",
1584
- backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})`,
1585
- boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04), 0 0 0 2px rgba(255,255,255,0.7)",
1586
- zIndex: 9990
1587
- },
1723
+ className: `qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-white qa-print-hidden qa-fab-btn${coarse ? " qa-touch-none" : ""}`,
1724
+ style: fabStyle,
1588
1725
  children: [
1589
1726
  !isOpen && /* @__PURE__ */ jsx(
1590
1727
  "span",
@@ -2547,6 +2684,20 @@ function panelReducer(state, action) {
2547
2684
  return state;
2548
2685
  }
2549
2686
  }
2687
+ var KEYBOARD_OVERLAP_THRESHOLD = 120;
2688
+ var KEYBOARD_LIFT_GAP = 12;
2689
+ var NON_TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
2690
+ "checkbox",
2691
+ "radio",
2692
+ "range",
2693
+ "button",
2694
+ "submit",
2695
+ "reset",
2696
+ "color",
2697
+ "file",
2698
+ "image"
2699
+ ]);
2700
+ var PANEL_TRANSITION_WITH_LIFT = "opacity 200ms cubic-bezier(0.4,0,0.2,1), transform 200ms cubic-bezier(0.4,0,0.2,1), bottom 200ms cubic-bezier(0.4,0,0.2,1)";
2550
2701
  function QaPanel() {
2551
2702
  const {
2552
2703
  isOpen,
@@ -2612,6 +2763,61 @@ function QaPanel() {
2612
2763
  mql.addListener(handleChange);
2613
2764
  return () => mql.removeListener(handleChange);
2614
2765
  }, []);
2766
+ const coarse = useCoarsePointer();
2767
+ const panelRef = useRef(null);
2768
+ const [keyboardLift, setKeyboardLift] = useState(0);
2769
+ const computeKeyboardLift = useCallback(() => {
2770
+ try {
2771
+ if (!coarse || isIpadLandscape) return 0;
2772
+ if (typeof window === "undefined") return 0;
2773
+ const vv = window.visualViewport;
2774
+ if (!vv) return 0;
2775
+ const overlap = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
2776
+ if (overlap <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
2777
+ const panel = panelRef.current;
2778
+ if (!panel) return 0;
2779
+ const root = panel.getRootNode();
2780
+ const active = root.activeElement;
2781
+ if (!active || !panel.contains(active)) return 0;
2782
+ const tag = active.tagName;
2783
+ if (tag === "TEXTAREA") return Math.round(overlap) + KEYBOARD_LIFT_GAP;
2784
+ if (tag === "INPUT" && !NON_TEXT_INPUT_TYPES.has(active.type)) {
2785
+ return Math.round(overlap) + KEYBOARD_LIFT_GAP;
2786
+ }
2787
+ return 0;
2788
+ } catch {
2789
+ return 0;
2790
+ }
2791
+ }, [coarse, isIpadLandscape]);
2792
+ useEffect(() => {
2793
+ if (!coarse) return void 0;
2794
+ if (typeof window === "undefined" || typeof document === "undefined") return void 0;
2795
+ const vv = window.visualViewport;
2796
+ if (!vv) return void 0;
2797
+ let closeTimer;
2798
+ const recompute = () => setKeyboardLift(computeKeyboardLift());
2799
+ const recomputeSoon = () => {
2800
+ if (closeTimer !== void 0) clearTimeout(closeTimer);
2801
+ closeTimer = setTimeout(recompute, 80);
2802
+ };
2803
+ recompute();
2804
+ vv.addEventListener("resize", recompute);
2805
+ vv.addEventListener("scroll", recompute);
2806
+ document.addEventListener("focusin", recompute);
2807
+ document.addEventListener("focusout", recomputeSoon);
2808
+ return () => {
2809
+ if (closeTimer !== void 0) clearTimeout(closeTimer);
2810
+ vv.removeEventListener("resize", recompute);
2811
+ vv.removeEventListener("scroll", recompute);
2812
+ document.removeEventListener("focusin", recompute);
2813
+ document.removeEventListener("focusout", recomputeSoon);
2814
+ };
2815
+ }, [coarse, computeKeyboardLift]);
2816
+ useEffect(() => {
2817
+ if (!isOpen) setKeyboardLift(0);
2818
+ }, [isOpen]);
2819
+ const keyboardLiftActive = coarse && !isIpadLandscape;
2820
+ const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
2615
2821
  if (phase === "hidden") return null;
2616
2822
  const openNaming = () => {
2617
2823
  setFilename(todayName());
@@ -2622,9 +2828,12 @@ function QaPanel() {
2622
2828
  void exportZip(filename);
2623
2829
  };
2624
2830
  const namingCoverage = naming ? computeCoverage(journey, guideChecked) : null;
2831
+ const restBottomRem = dir === "rtl" ? "9rem" : "8.75rem";
2832
+ const panelBottom = isIpadLandscape ? "0" : appliedKeyboardLift > 0 ? `calc(${restBottomRem} + env(safe-area-inset-bottom) + ${appliedKeyboardLift}px)` : `calc(${restBottomRem} + env(safe-area-inset-bottom))`;
2625
2833
  return /* @__PURE__ */ jsxs(
2626
2834
  "div",
2627
2835
  {
2836
+ ref: panelRef,
2628
2837
  "data-qa-overlay": "true",
2629
2838
  dir,
2630
2839
  onTransitionEnd: handleTransitionEnd,
@@ -2635,7 +2844,7 @@ function QaPanel() {
2635
2844
  left: isIpadLandscape ? "auto" : "calc(1rem + env(safe-area-inset-left))",
2636
2845
  right: isIpadLandscape ? "0" : void 0,
2637
2846
  top: isIpadLandscape ? "0" : void 0,
2638
- bottom: isIpadLandscape ? "0" : dir === "rtl" ? "calc(9rem + env(safe-area-inset-bottom))" : "calc(8.75rem + env(safe-area-inset-bottom))",
2847
+ bottom: panelBottom,
2639
2848
  height: isIpadLandscape ? "100dvh" : void 0,
2640
2849
  width: isIpadLandscape ? "min(92vw, 420px)" : void 0,
2641
2850
  // qa-max-h-74vh (class) would otherwise cap the sheet well short of
@@ -2645,7 +2854,12 @@ function QaPanel() {
2645
2854
  background: theme.surface,
2646
2855
  borderColor: `${theme.primary}22`,
2647
2856
  fontFamily: lang === "ar" ? "'Tajawal', sans-serif" : "'Nunito', system-ui, sans-serif",
2648
- zIndex: 9990
2857
+ zIndex: 9990,
2858
+ // Keyboard-avoidance lift (coarse/touch only — see effect above).
2859
+ // undefined ⇒ !keyboardLiftActive, so desktop and the iPad-landscape
2860
+ // side-sheet render this property exactly as before (the class's own
2861
+ // opacity/transform transition applies, untouched).
2862
+ transition: keyboardLiftActive ? PANEL_TRANSITION_WITH_LIFT : void 0
2649
2863
  },
2650
2864
  children: [
2651
2865
  /* @__PURE__ */ jsxs(
@@ -3029,29 +3243,6 @@ function getStableSelector(el) {
3029
3243
  }
3030
3244
  return nthOfTypePath(el);
3031
3245
  }
3032
- function isCoarsePointer() {
3033
- if (typeof window === "undefined") return false;
3034
- try {
3035
- if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
3036
- } catch {
3037
- }
3038
- return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
3039
- }
3040
- function useCoarsePointer() {
3041
- const [coarse, setCoarse] = useState(() => isCoarsePointer());
3042
- useEffect(() => {
3043
- if (typeof window === "undefined" || !window.matchMedia) return;
3044
- const mq = window.matchMedia("(pointer: coarse)");
3045
- const on = () => setCoarse(isCoarsePointer());
3046
- if (mq.addEventListener) mq.addEventListener("change", on);
3047
- else if (mq.addListener) mq.addListener(on);
3048
- return () => {
3049
- if (mq.removeEventListener) mq.removeEventListener("change", on);
3050
- else if (mq.removeListener) mq.removeListener(on);
3051
- };
3052
- }, []);
3053
- return coarse;
3054
- }
3055
3246
 
3056
3247
  // src/lib/scrollLock.ts
3057
3248
  var locked = false;
@@ -3073,7 +3264,7 @@ function unlockPageScroll() {
3073
3264
  if (document.body) document.body.style.overflow = prevBodyOverflow;
3074
3265
  locked = false;
3075
3266
  }
3076
- var DRAG_THRESHOLD = 6;
3267
+ var DRAG_THRESHOLD2 = 6;
3077
3268
  var TOUCH_DRAG_THRESHOLD = 12;
3078
3269
  var MIN_REGION_SIZE = 8;
3079
3270
  var REGION_HANDLES = [
@@ -3196,7 +3387,7 @@ function CaptureMode() {
3196
3387
  activePointerId.current = null;
3197
3388
  const d = dragRef.current;
3198
3389
  dragRef.current = null;
3199
- const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD : TOUCH_DRAG_THRESHOLD;
3390
+ const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD2 : TOUCH_DRAG_THRESHOLD;
3200
3391
  const moved = d !== null && Math.hypot(e.clientX - d.x0, e.clientY - d.y0) > threshold;
3201
3392
  scrollSnap.current = { x: window.scrollX, y: window.scrollY };
3202
3393
  if (moved && d) {
@@ -3819,5 +4010,5 @@ function Qapture({ config }) {
3819
4010
  }
3820
4011
 
3821
4012
  export { Qapture, initQaStudio };
3822
- //# sourceMappingURL=chunk-OPZF4IVW.js.map
3823
- //# sourceMappingURL=chunk-OPZF4IVW.js.map
4013
+ //# sourceMappingURL=chunk-BE3H3FKR.js.map
4014
+ //# sourceMappingURL=chunk-BE3H3FKR.js.map
@@ -1570,28 +1570,165 @@ function Icon({
1570
1570
  function renderSvgElement(tag, attrs, key) {
1571
1571
  return React__default.default.createElement(tag, { key, ...attrs });
1572
1572
  }
1573
+ function isCoarsePointer() {
1574
+ if (typeof window === "undefined") return false;
1575
+ try {
1576
+ if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
1577
+ } catch {
1578
+ }
1579
+ return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
1580
+ }
1581
+ function useCoarsePointer() {
1582
+ const [coarse, setCoarse] = React.useState(() => isCoarsePointer());
1583
+ React.useEffect(() => {
1584
+ if (typeof window === "undefined" || !window.matchMedia) return;
1585
+ const mq = window.matchMedia("(pointer: coarse)");
1586
+ const on = () => setCoarse(isCoarsePointer());
1587
+ if (mq.addEventListener) mq.addEventListener("change", on);
1588
+ else if (mq.addListener) mq.addListener(on);
1589
+ return () => {
1590
+ if (mq.removeEventListener) mq.removeEventListener("change", on);
1591
+ else if (mq.removeListener) mq.removeListener(on);
1592
+ };
1593
+ }, []);
1594
+ return coarse;
1595
+ }
1596
+ var DEFAULT_LEFT = "calc(1.25rem + env(safe-area-inset-left))";
1597
+ var DEFAULT_BOTTOM = "calc(5rem + env(safe-area-inset-bottom))";
1598
+ var FAB_SIZE_PX = 56;
1599
+ var EDGE_MARGIN = 12;
1600
+ var DRAG_THRESHOLD = 8;
1601
+ var FAB_POS_KEY = "qapture:fabpos";
1602
+ function isFabPos(v) {
1603
+ if (!v || typeof v !== "object") return false;
1604
+ const o = v;
1605
+ return typeof o.left === "number" && Number.isFinite(o.left) && typeof o.bottom === "number" && Number.isFinite(o.bottom);
1606
+ }
1607
+ function loadFabPos() {
1608
+ if (typeof window === "undefined") return null;
1609
+ try {
1610
+ const raw = window.localStorage.getItem(FAB_POS_KEY);
1611
+ if (!raw) return null;
1612
+ const parsed = JSON.parse(raw);
1613
+ return isFabPos(parsed) ? parsed : null;
1614
+ } catch {
1615
+ return null;
1616
+ }
1617
+ }
1618
+ function saveFabPos(pos) {
1619
+ if (typeof window === "undefined") return;
1620
+ try {
1621
+ window.localStorage.setItem(FAB_POS_KEY, JSON.stringify(pos));
1622
+ } catch {
1623
+ }
1624
+ }
1625
+ function clampNum(v, lo, hi) {
1626
+ return Math.min(Math.max(v, lo), hi);
1627
+ }
1628
+ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
1629
+ if (typeof window === "undefined") return p;
1630
+ const maxLeft = Math.max(EDGE_MARGIN, window.innerWidth - w - EDGE_MARGIN);
1631
+ const maxBottom = Math.max(EDGE_MARGIN, window.innerHeight - h - EDGE_MARGIN);
1632
+ return {
1633
+ left: clampNum(p.left, EDGE_MARGIN, maxLeft),
1634
+ bottom: clampNum(p.bottom, EDGE_MARGIN, maxBottom)
1635
+ };
1636
+ }
1573
1637
  function QaFab() {
1574
1638
  const { isOpen, setIsOpen, notes, captureActive, theme } = useQa();
1639
+ const coarse = useCoarsePointer();
1640
+ const [pos, setPos] = React.useState(() => loadFabPos());
1641
+ const dragRef = React.useRef(null);
1642
+ const didDragRef = React.useRef(false);
1575
1643
  if (captureActive) return null;
1644
+ const onPointerDown = (e) => {
1645
+ if (dragRef.current) return;
1646
+ didDragRef.current = false;
1647
+ const target = e.currentTarget;
1648
+ const rect = target.getBoundingClientRect();
1649
+ dragRef.current = {
1650
+ pointerId: e.pointerId,
1651
+ startX: e.clientX,
1652
+ startY: e.clientY,
1653
+ startLeft: rect.left,
1654
+ startBottom: window.innerHeight - rect.bottom,
1655
+ width: rect.width,
1656
+ height: rect.height,
1657
+ dragging: false
1658
+ };
1659
+ try {
1660
+ target.setPointerCapture(e.pointerId);
1661
+ } catch {
1662
+ }
1663
+ };
1664
+ const onPointerMove = (e) => {
1665
+ const d = dragRef.current;
1666
+ if (!d || d.pointerId !== e.pointerId) return;
1667
+ const dx = e.clientX - d.startX;
1668
+ const dy = e.clientY - d.startY;
1669
+ if (!d.dragging) {
1670
+ if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
1671
+ d.dragging = true;
1672
+ }
1673
+ setPos(clampFabPos({ left: d.startLeft + dx, bottom: d.startBottom - dy }, d.width, d.height));
1674
+ };
1675
+ const endDrag = (e) => {
1676
+ const d = dragRef.current;
1677
+ if (!d || d.pointerId !== e.pointerId) return null;
1678
+ try {
1679
+ e.currentTarget.releasePointerCapture(e.pointerId);
1680
+ } catch {
1681
+ }
1682
+ dragRef.current = null;
1683
+ return d;
1684
+ };
1685
+ const onPointerUp = (e) => {
1686
+ const d = endDrag(e);
1687
+ if (!d) return;
1688
+ if (d.dragging) {
1689
+ const dx = e.clientX - d.startX;
1690
+ const dy = e.clientY - d.startY;
1691
+ const next = clampFabPos({ left: d.startLeft + dx, bottom: d.startBottom - dy }, d.width, d.height);
1692
+ setPos(next);
1693
+ saveFabPos(next);
1694
+ didDragRef.current = true;
1695
+ }
1696
+ };
1697
+ const onPointerCancel = (e) => {
1698
+ endDrag(e);
1699
+ };
1700
+ const handleClick = () => {
1701
+ if (didDragRef.current) {
1702
+ didDragRef.current = false;
1703
+ return;
1704
+ }
1705
+ setIsOpen(!isOpen);
1706
+ };
1707
+ const applied = coarse && pos ? clampFabPos(pos) : null;
1708
+ const fabStyle = {
1709
+ left: applied ? `${applied.left}px` : DEFAULT_LEFT,
1710
+ bottom: applied ? `${applied.bottom}px` : DEFAULT_BOTTOM,
1711
+ width: "3.5rem",
1712
+ height: "3.5rem",
1713
+ backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})`,
1714
+ boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04), 0 0 0 2px rgba(255,255,255,0.7)",
1715
+ zIndex: 9990
1716
+ };
1576
1717
  return /* @__PURE__ */ jsxRuntime.jsxs(
1577
1718
  "button",
1578
1719
  {
1579
1720
  type: "button",
1580
1721
  "data-qa-overlay": "true",
1581
1722
  dir: "ltr",
1582
- onClick: () => setIsOpen(!isOpen),
1723
+ onClick: handleClick,
1724
+ onPointerDown: coarse ? onPointerDown : void 0,
1725
+ onPointerMove: coarse ? onPointerMove : void 0,
1726
+ onPointerUp: coarse ? onPointerUp : void 0,
1727
+ onPointerCancel: coarse ? onPointerCancel : void 0,
1583
1728
  "aria-label": "Qapture \u2014 testing notes",
1584
1729
  title: "Qapture",
1585
- className: "qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-white qa-print-hidden qa-fab-btn",
1586
- style: {
1587
- left: "calc(1.25rem + env(safe-area-inset-left))",
1588
- bottom: "calc(5rem + env(safe-area-inset-bottom))",
1589
- width: "3.5rem",
1590
- height: "3.5rem",
1591
- backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})`,
1592
- boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04), 0 0 0 2px rgba(255,255,255,0.7)",
1593
- zIndex: 9990
1594
- },
1730
+ className: `qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-white qa-print-hidden qa-fab-btn${coarse ? " qa-touch-none" : ""}`,
1731
+ style: fabStyle,
1595
1732
  children: [
1596
1733
  !isOpen && /* @__PURE__ */ jsxRuntime.jsx(
1597
1734
  "span",
@@ -2554,6 +2691,20 @@ function panelReducer(state, action) {
2554
2691
  return state;
2555
2692
  }
2556
2693
  }
2694
+ var KEYBOARD_OVERLAP_THRESHOLD = 120;
2695
+ var KEYBOARD_LIFT_GAP = 12;
2696
+ var NON_TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
2697
+ "checkbox",
2698
+ "radio",
2699
+ "range",
2700
+ "button",
2701
+ "submit",
2702
+ "reset",
2703
+ "color",
2704
+ "file",
2705
+ "image"
2706
+ ]);
2707
+ var PANEL_TRANSITION_WITH_LIFT = "opacity 200ms cubic-bezier(0.4,0,0.2,1), transform 200ms cubic-bezier(0.4,0,0.2,1), bottom 200ms cubic-bezier(0.4,0,0.2,1)";
2557
2708
  function QaPanel() {
2558
2709
  const {
2559
2710
  isOpen,
@@ -2619,6 +2770,61 @@ function QaPanel() {
2619
2770
  mql.addListener(handleChange);
2620
2771
  return () => mql.removeListener(handleChange);
2621
2772
  }, []);
2773
+ const coarse = useCoarsePointer();
2774
+ const panelRef = React.useRef(null);
2775
+ const [keyboardLift, setKeyboardLift] = React.useState(0);
2776
+ const computeKeyboardLift = React.useCallback(() => {
2777
+ try {
2778
+ if (!coarse || isIpadLandscape) return 0;
2779
+ if (typeof window === "undefined") return 0;
2780
+ const vv = window.visualViewport;
2781
+ if (!vv) return 0;
2782
+ const overlap = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
2783
+ if (overlap <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
2784
+ const panel = panelRef.current;
2785
+ if (!panel) return 0;
2786
+ const root = panel.getRootNode();
2787
+ const active = root.activeElement;
2788
+ if (!active || !panel.contains(active)) return 0;
2789
+ const tag = active.tagName;
2790
+ if (tag === "TEXTAREA") return Math.round(overlap) + KEYBOARD_LIFT_GAP;
2791
+ if (tag === "INPUT" && !NON_TEXT_INPUT_TYPES.has(active.type)) {
2792
+ return Math.round(overlap) + KEYBOARD_LIFT_GAP;
2793
+ }
2794
+ return 0;
2795
+ } catch {
2796
+ return 0;
2797
+ }
2798
+ }, [coarse, isIpadLandscape]);
2799
+ React.useEffect(() => {
2800
+ if (!coarse) return void 0;
2801
+ if (typeof window === "undefined" || typeof document === "undefined") return void 0;
2802
+ const vv = window.visualViewport;
2803
+ if (!vv) return void 0;
2804
+ let closeTimer;
2805
+ const recompute = () => setKeyboardLift(computeKeyboardLift());
2806
+ const recomputeSoon = () => {
2807
+ if (closeTimer !== void 0) clearTimeout(closeTimer);
2808
+ closeTimer = setTimeout(recompute, 80);
2809
+ };
2810
+ recompute();
2811
+ vv.addEventListener("resize", recompute);
2812
+ vv.addEventListener("scroll", recompute);
2813
+ document.addEventListener("focusin", recompute);
2814
+ document.addEventListener("focusout", recomputeSoon);
2815
+ return () => {
2816
+ if (closeTimer !== void 0) clearTimeout(closeTimer);
2817
+ vv.removeEventListener("resize", recompute);
2818
+ vv.removeEventListener("scroll", recompute);
2819
+ document.removeEventListener("focusin", recompute);
2820
+ document.removeEventListener("focusout", recomputeSoon);
2821
+ };
2822
+ }, [coarse, computeKeyboardLift]);
2823
+ React.useEffect(() => {
2824
+ if (!isOpen) setKeyboardLift(0);
2825
+ }, [isOpen]);
2826
+ const keyboardLiftActive = coarse && !isIpadLandscape;
2827
+ const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
2622
2828
  if (phase === "hidden") return null;
2623
2829
  const openNaming = () => {
2624
2830
  setFilename(todayName());
@@ -2629,9 +2835,12 @@ function QaPanel() {
2629
2835
  void exportZip(filename);
2630
2836
  };
2631
2837
  const namingCoverage = naming ? computeCoverage(journey, guideChecked) : null;
2838
+ const restBottomRem = dir === "rtl" ? "9rem" : "8.75rem";
2839
+ const panelBottom = isIpadLandscape ? "0" : appliedKeyboardLift > 0 ? `calc(${restBottomRem} + env(safe-area-inset-bottom) + ${appliedKeyboardLift}px)` : `calc(${restBottomRem} + env(safe-area-inset-bottom))`;
2632
2840
  return /* @__PURE__ */ jsxRuntime.jsxs(
2633
2841
  "div",
2634
2842
  {
2843
+ ref: panelRef,
2635
2844
  "data-qa-overlay": "true",
2636
2845
  dir,
2637
2846
  onTransitionEnd: handleTransitionEnd,
@@ -2642,7 +2851,7 @@ function QaPanel() {
2642
2851
  left: isIpadLandscape ? "auto" : "calc(1rem + env(safe-area-inset-left))",
2643
2852
  right: isIpadLandscape ? "0" : void 0,
2644
2853
  top: isIpadLandscape ? "0" : void 0,
2645
- bottom: isIpadLandscape ? "0" : dir === "rtl" ? "calc(9rem + env(safe-area-inset-bottom))" : "calc(8.75rem + env(safe-area-inset-bottom))",
2854
+ bottom: panelBottom,
2646
2855
  height: isIpadLandscape ? "100dvh" : void 0,
2647
2856
  width: isIpadLandscape ? "min(92vw, 420px)" : void 0,
2648
2857
  // qa-max-h-74vh (class) would otherwise cap the sheet well short of
@@ -2652,7 +2861,12 @@ function QaPanel() {
2652
2861
  background: theme.surface,
2653
2862
  borderColor: `${theme.primary}22`,
2654
2863
  fontFamily: lang === "ar" ? "'Tajawal', sans-serif" : "'Nunito', system-ui, sans-serif",
2655
- zIndex: 9990
2864
+ zIndex: 9990,
2865
+ // Keyboard-avoidance lift (coarse/touch only — see effect above).
2866
+ // undefined ⇒ !keyboardLiftActive, so desktop and the iPad-landscape
2867
+ // side-sheet render this property exactly as before (the class's own
2868
+ // opacity/transform transition applies, untouched).
2869
+ transition: keyboardLiftActive ? PANEL_TRANSITION_WITH_LIFT : void 0
2656
2870
  },
2657
2871
  children: [
2658
2872
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3036,29 +3250,6 @@ function getStableSelector(el) {
3036
3250
  }
3037
3251
  return nthOfTypePath(el);
3038
3252
  }
3039
- function isCoarsePointer() {
3040
- if (typeof window === "undefined") return false;
3041
- try {
3042
- if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
3043
- } catch {
3044
- }
3045
- return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
3046
- }
3047
- function useCoarsePointer() {
3048
- const [coarse, setCoarse] = React.useState(() => isCoarsePointer());
3049
- React.useEffect(() => {
3050
- if (typeof window === "undefined" || !window.matchMedia) return;
3051
- const mq = window.matchMedia("(pointer: coarse)");
3052
- const on = () => setCoarse(isCoarsePointer());
3053
- if (mq.addEventListener) mq.addEventListener("change", on);
3054
- else if (mq.addListener) mq.addListener(on);
3055
- return () => {
3056
- if (mq.removeEventListener) mq.removeEventListener("change", on);
3057
- else if (mq.removeListener) mq.removeListener(on);
3058
- };
3059
- }, []);
3060
- return coarse;
3061
- }
3062
3253
 
3063
3254
  // src/lib/scrollLock.ts
3064
3255
  var locked = false;
@@ -3080,7 +3271,7 @@ function unlockPageScroll() {
3080
3271
  if (document.body) document.body.style.overflow = prevBodyOverflow;
3081
3272
  locked = false;
3082
3273
  }
3083
- var DRAG_THRESHOLD = 6;
3274
+ var DRAG_THRESHOLD2 = 6;
3084
3275
  var TOUCH_DRAG_THRESHOLD = 12;
3085
3276
  var MIN_REGION_SIZE = 8;
3086
3277
  var REGION_HANDLES = [
@@ -3203,7 +3394,7 @@ function CaptureMode() {
3203
3394
  activePointerId.current = null;
3204
3395
  const d = dragRef.current;
3205
3396
  dragRef.current = null;
3206
- const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD : TOUCH_DRAG_THRESHOLD;
3397
+ const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD2 : TOUCH_DRAG_THRESHOLD;
3207
3398
  const moved = d !== null && Math.hypot(e.clientX - d.x0, e.clientY - d.y0) > threshold;
3208
3399
  scrollSnap.current = { x: window.scrollX, y: window.scrollY };
3209
3400
  if (moved && d) {
@@ -3827,5 +4018,5 @@ function Qapture({ config }) {
3827
4018
 
3828
4019
  exports.Qapture = Qapture;
3829
4020
  exports.initQaStudio = initQaStudio;
3830
- //# sourceMappingURL=chunk-CJQHPDT7.cjs.map
3831
- //# sourceMappingURL=chunk-CJQHPDT7.cjs.map
4021
+ //# sourceMappingURL=chunk-PC6LNG5Y.cjs.map
4022
+ //# sourceMappingURL=chunk-PC6LNG5Y.cjs.map
package/dist/index.cjs CHANGED
@@ -1,20 +1,20 @@
1
1
  'use strict';
2
2
 
3
- var chunkCJQHPDT7_cjs = require('./chunk-CJQHPDT7.cjs');
3
+ var chunkPC6LNG5Y_cjs = require('./chunk-PC6LNG5Y.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "QaStudio", {
8
8
  enumerable: true,
9
- get: function () { return chunkCJQHPDT7_cjs.Qapture; }
9
+ get: function () { return chunkPC6LNG5Y_cjs.Qapture; }
10
10
  });
11
11
  Object.defineProperty(exports, "Qapture", {
12
12
  enumerable: true,
13
- get: function () { return chunkCJQHPDT7_cjs.Qapture; }
13
+ get: function () { return chunkPC6LNG5Y_cjs.Qapture; }
14
14
  });
15
15
  Object.defineProperty(exports, "initQaStudio", {
16
16
  enumerable: true,
17
- get: function () { return chunkCJQHPDT7_cjs.initQaStudio; }
17
+ get: function () { return chunkPC6LNG5Y_cjs.initQaStudio; }
18
18
  });
19
19
  //# sourceMappingURL=index.cjs.map
20
20
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.cts CHANGED
@@ -165,11 +165,11 @@ declare function initQaStudio(config?: QaConfig): {
165
165
  * (destroy + remount if needed).
166
166
  *
167
167
  * Usage (Next.js App Router):
168
- * import { Qapture } from 'qapture/next'; // adds 'use client' banner
168
+ * import { Qapture } from 'qapture2/next'; // adds 'use client' banner
169
169
  * <Qapture config={qaConfig} />
170
170
  *
171
171
  * Usage (any React app):
172
- * import { Qapture } from 'qapture';
172
+ * import { Qapture } from 'qapture2';
173
173
  * <Qapture config={qaConfig} />
174
174
  */
175
175
  declare function Qapture({ config }: {
package/dist/index.d.ts CHANGED
@@ -165,11 +165,11 @@ declare function initQaStudio(config?: QaConfig): {
165
165
  * (destroy + remount if needed).
166
166
  *
167
167
  * Usage (Next.js App Router):
168
- * import { Qapture } from 'qapture/next'; // adds 'use client' banner
168
+ * import { Qapture } from 'qapture2/next'; // adds 'use client' banner
169
169
  * <Qapture config={qaConfig} />
170
170
  *
171
171
  * Usage (any React app):
172
- * import { Qapture } from 'qapture';
172
+ * import { Qapture } from 'qapture2';
173
173
  * <Qapture config={qaConfig} />
174
174
  */
175
175
  declare function Qapture({ config }: {
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-OPZF4IVW.js';
1
+ export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-BE3H3FKR.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
package/dist/next.cjs CHANGED
@@ -1,21 +1,21 @@
1
1
  "use client";
2
2
  'use strict';
3
3
 
4
- var chunkCJQHPDT7_cjs = require('./chunk-CJQHPDT7.cjs');
4
+ var chunkPC6LNG5Y_cjs = require('./chunk-PC6LNG5Y.cjs');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "QaStudio", {
9
9
  enumerable: true,
10
- get: function () { return chunkCJQHPDT7_cjs.Qapture; }
10
+ get: function () { return chunkPC6LNG5Y_cjs.Qapture; }
11
11
  });
12
12
  Object.defineProperty(exports, "Qapture", {
13
13
  enumerable: true,
14
- get: function () { return chunkCJQHPDT7_cjs.Qapture; }
14
+ get: function () { return chunkPC6LNG5Y_cjs.Qapture; }
15
15
  });
16
16
  Object.defineProperty(exports, "initQaStudio", {
17
17
  enumerable: true,
18
- get: function () { return chunkCJQHPDT7_cjs.initQaStudio; }
18
+ get: function () { return chunkPC6LNG5Y_cjs.initQaStudio; }
19
19
  });
20
20
  //# sourceMappingURL=next.cjs.map
21
21
  //# sourceMappingURL=next.cjs.map
package/dist/next.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use client";
2
- export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-OPZF4IVW.js';
2
+ export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-BE3H3FKR.js';
3
3
  //# sourceMappingURL=next.js.map
4
4
  //# sourceMappingURL=next.js.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkCJQHPDT7_cjs = require('./chunk-CJQHPDT7.cjs');
3
+ var chunkPC6LNG5Y_cjs = require('./chunk-PC6LNG5Y.cjs');
4
4
 
5
5
  // src/standalone.ts
6
6
  if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {
@@ -33,7 +33,7 @@ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !c
33
33
 
34
34
  Object.defineProperty(exports, "initQaStudio", {
35
35
  enumerable: true,
36
- get: function () { return chunkCJQHPDT7_cjs.initQaStudio; }
36
+ get: function () { return chunkPC6LNG5Y_cjs.initQaStudio; }
37
37
  });
38
38
  //# sourceMappingURL=standalone.cjs.map
39
39
  //# sourceMappingURL=standalone.cjs.map
@@ -1,4 +1,4 @@
1
- export { initQaStudio } from './chunk-OPZF4IVW.js';
1
+ export { initQaStudio } from './chunk-BE3H3FKR.js';
2
2
 
3
3
  // src/standalone.ts
4
4
  if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qapture2",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Drop-in, AI-aware in-browser QA capture widget. A tester annotates the live app (element/region + auto-screenshot + note), tracks a graded testing journey, and exports a ZIP your own coding agent reads. Ships zero AI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@
55
55
  "./package.json": "./package.json"
56
56
  },
57
57
  "bin": {
58
- "qapture2": "./dist/bin/init.cjs"
58
+ "qapture2": "dist/bin/init.cjs"
59
59
  },
60
60
  "repository": {
61
61
  "type": "git",