qapture2 0.7.0 → 0.7.1

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.
@@ -1518,6 +1518,128 @@ function matchRouteToSteps(journey, route) {
1518
1518
  return [...exact, ...param];
1519
1519
  }
1520
1520
 
1521
+ // src/lib/fallbackJourney.ts
1522
+ var ANY_PAGE = "*";
1523
+ var FALLBACK_JOURNEY = [
1524
+ {
1525
+ id: "first-run",
1526
+ role: { en: "First look", ar: "\u0627\u0644\u0646\u0638\u0631\u0629 \u0627\u0644\u0623\u0648\u0644\u0649" },
1527
+ steps: [
1528
+ {
1529
+ path: ANY_PAGE,
1530
+ what: { en: "Open the app fresh and let it finish loading", ar: "\u0627\u0641\u062A\u062D \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0645\u0646 \u062C\u062F\u064A\u062F \u0648\u0627\u062A\u0631\u0643\u0647 \u064A\u0643\u0645\u0644 \u0627\u0644\u062A\u062D\u0645\u064A\u0644" },
1531
+ expect: {
1532
+ en: "Content appears, nothing flashes or jumps, no error in the corner",
1533
+ ar: "\u064A\u0638\u0647\u0631 \u0627\u0644\u0645\u062D\u062A\u0648\u0649 \u062F\u0648\u0646 \u0648\u0645\u064A\u0636 \u0623\u0648 \u0642\u0641\u0632\u060C \u0648\u0628\u062F\u0648\u0646 \u0631\u0633\u0627\u0644\u0629 \u062E\u0637\u0623"
1534
+ },
1535
+ risk: "red"
1536
+ },
1537
+ {
1538
+ path: ANY_PAGE,
1539
+ what: { en: "Read the first screen as a new user would", ar: "\u0627\u0642\u0631\u0623 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0623\u0648\u0644\u0649 \u0628\u0639\u064A\u0646 \u0645\u0633\u062A\u062E\u062F\u0645 \u062C\u062F\u064A\u062F" },
1540
+ expect: {
1541
+ en: "It is obvious what this is and what to do next",
1542
+ ar: "\u0648\u0627\u0636\u062D \u0645\u0627 \u0647\u0630\u0627 \u0648\u0645\u0627 \u0627\u0644\u062E\u0637\u0648\u0629 \u0627\u0644\u062A\u0627\u0644\u064A\u0629"
1543
+ },
1544
+ risk: "amber"
1545
+ }
1546
+ ]
1547
+ },
1548
+ {
1549
+ id: "moving-around",
1550
+ role: { en: "Moving around", ar: "\u0627\u0644\u062A\u0646\u0642\u0644" },
1551
+ steps: [
1552
+ {
1553
+ path: ANY_PAGE,
1554
+ what: { en: "Visit every item in the main menu", ar: "\u0627\u0641\u062A\u062D \u0643\u0644 \u0639\u0646\u0635\u0631 \u0641\u064A \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629" },
1555
+ expect: { en: "Each one opens the page it names \u2014 no blank screens", ar: "\u0643\u0644 \u0639\u0646\u0635\u0631 \u064A\u0641\u062A\u062D \u0635\u0641\u062D\u062A\u0647 \u2014 \u0628\u062F\u0648\u0646 \u0634\u0627\u0634\u0627\u062A \u0641\u0627\u0631\u063A\u0629" },
1556
+ risk: "red"
1557
+ },
1558
+ {
1559
+ path: ANY_PAGE,
1560
+ what: { en: "Press the browser Back button after moving", ar: "\u0627\u0636\u063A\u0637 \u0632\u0631 \u0627\u0644\u0631\u062C\u0648\u0639 \u0641\u064A \u0627\u0644\u0645\u062A\u0635\u0641\u062D \u0628\u0639\u062F \u0627\u0644\u062A\u0646\u0642\u0644" },
1561
+ expect: { en: "You land where you were, with your place kept", ar: "\u062A\u0639\u0648\u062F \u0625\u0644\u0649 \u062D\u064A\u062B \u0643\u0646\u062A \u0645\u0639 \u0628\u0642\u0627\u0621 \u0645\u0648\u0636\u0639\u0643" },
1562
+ risk: "amber"
1563
+ },
1564
+ {
1565
+ path: ANY_PAGE,
1566
+ what: { en: "Reload the page you are on", ar: "\u0623\u0639\u062F \u062A\u062D\u0645\u064A\u0644 \u0627\u0644\u0635\u0641\u062D\u0629 \u0627\u0644\u062D\u0627\u0644\u064A\u0629" },
1567
+ expect: { en: "The same page comes back, still signed in", ar: "\u062A\u0639\u0648\u062F \u0627\u0644\u0635\u0641\u062D\u0629 \u0646\u0641\u0633\u0647\u0627 \u0648\u064A\u0628\u0642\u0649 \u062A\u0633\u062C\u064A\u0644 \u0627\u0644\u062F\u062E\u0648\u0644" },
1568
+ risk: "red"
1569
+ }
1570
+ ]
1571
+ },
1572
+ {
1573
+ id: "the-main-thing",
1574
+ role: { en: "The main thing", ar: "\u0627\u0644\u0645\u0647\u0645\u0629 \u0627\u0644\u0623\u0633\u0627\u0633\u064A\u0629" },
1575
+ steps: [
1576
+ {
1577
+ path: ANY_PAGE,
1578
+ what: {
1579
+ en: "Do what this app is for, start to finish, without shortcuts",
1580
+ ar: "\u0646\u0641\u0651\u0630 \u0627\u0644\u063A\u0631\u0636 \u0627\u0644\u0623\u0633\u0627\u0633\u064A \u0645\u0646 \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0645\u0646 \u0627\u0644\u0628\u062F\u0627\u064A\u0629 \u0644\u0644\u0646\u0647\u0627\u064A\u0629 \u062F\u0648\u0646 \u0627\u062E\u062A\u0635\u0627\u0631\u0627\u062A"
1581
+ },
1582
+ expect: { en: "It completes, and you are told clearly that it did", ar: "\u062A\u0643\u062A\u0645\u0644 \u0627\u0644\u0645\u0647\u0645\u0629 \u0648\u064A\u0635\u0644\u0643 \u062A\u0623\u0643\u064A\u062F \u0648\u0627\u0636\u062D" },
1583
+ risk: "red"
1584
+ },
1585
+ {
1586
+ path: ANY_PAGE,
1587
+ what: { en: "Do it a second time with different choices", ar: "\u0643\u0631\u0631\u0647\u0627 \u0645\u0631\u0629 \u062B\u0627\u0646\u064A\u0629 \u0628\u062E\u064A\u0627\u0631\u0627\u062A \u0645\u062E\u062A\u0644\u0641\u0629" },
1588
+ expect: { en: "Same result, nothing left over from the first run", ar: "\u0627\u0644\u0646\u062A\u064A\u062C\u0629 \u0646\u0641\u0633\u0647\u0627\u060C \u062F\u0648\u0646 \u0628\u0642\u0627\u064A\u0627 \u0645\u0646 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629 \u0627\u0644\u0623\u0648\u0644\u0649" },
1589
+ risk: "amber"
1590
+ }
1591
+ ]
1592
+ },
1593
+ {
1594
+ id: "when-it-goes-wrong",
1595
+ role: { en: "When it goes wrong", ar: "\u0639\u0646\u062F \u0627\u0644\u062E\u0637\u0623" },
1596
+ steps: [
1597
+ {
1598
+ path: ANY_PAGE,
1599
+ what: { en: "Submit a form empty, then with obviously wrong values", ar: "\u0623\u0631\u0633\u0644 \u0627\u0644\u0646\u0645\u0648\u0630\u062C \u0641\u0627\u0631\u063A\u064B\u0627 \u062B\u0645 \u0628\u0642\u064A\u0645 \u062E\u0627\u0637\u0626\u0629 \u0628\u0648\u0636\u0648\u062D" },
1600
+ expect: {
1601
+ en: "It refuses politely and says which field and why \u2014 it does not crash or save",
1602
+ ar: "\u064A\u0631\u0641\u0636 \u0628\u0644\u0637\u0641 \u0648\u064A\u0648\u0636\u062D \u0627\u0644\u062D\u0642\u0644 \u0648\u0627\u0644\u0633\u0628\u0628 \u2014 \u062F\u0648\u0646 \u062A\u0639\u0637\u0644 \u0623\u0648 \u062D\u0641\u0638"
1603
+ },
1604
+ risk: "red"
1605
+ },
1606
+ {
1607
+ path: ANY_PAGE,
1608
+ what: { en: "Press the main button twice, quickly", ar: "\u0627\u0636\u063A\u0637 \u0627\u0644\u0632\u0631 \u0627\u0644\u0631\u0626\u064A\u0633\u064A \u0645\u0631\u062A\u064A\u0646 \u0628\u0633\u0631\u0639\u0629" },
1609
+ expect: { en: "One result, not two", ar: "\u0646\u062A\u064A\u062C\u0629 \u0648\u0627\u062D\u062F\u0629 \u0644\u0627 \u0627\u062B\u0646\u062A\u0627\u0646" },
1610
+ risk: "amber"
1611
+ },
1612
+ {
1613
+ path: ANY_PAGE,
1614
+ what: { en: "Open a page you should not be allowed to see", ar: "\u0627\u0641\u062A\u062D \u0635\u0641\u062D\u0629 \u0644\u0627 \u064A\u064F\u0641\u062A\u0631\u0636 \u0623\u0646 \u062A\u0635\u0644 \u0625\u0644\u064A\u0647\u0627" },
1615
+ expect: { en: "You are turned away, not shown someone else\u2019s data", ar: "\u064A\u062A\u0645 \u0645\u0646\u0639\u0643\u060C \u0648\u0644\u0627 \u062A\u0638\u0647\u0631 \u0628\u064A\u0627\u0646\u0627\u062A \u0634\u062E\u0635 \u0622\u062E\u0631" },
1616
+ risk: "red"
1617
+ }
1618
+ ]
1619
+ },
1620
+ {
1621
+ id: "on-a-phone",
1622
+ role: { en: "On a phone", ar: "\u0639\u0644\u0649 \u0627\u0644\u0647\u0627\u062A\u0641" },
1623
+ steps: [
1624
+ {
1625
+ path: ANY_PAGE,
1626
+ what: { en: "Make the window narrow, or open it on a phone", ar: "\u0635\u063A\u0651\u0631 \u0627\u0644\u0646\u0627\u0641\u0630\u0629 \u0623\u0648 \u0627\u0641\u062A\u062D\u0647 \u0639\u0644\u0649 \u0627\u0644\u0647\u0627\u062A\u0641" },
1627
+ expect: {
1628
+ en: "Nothing is cut off, nothing overlaps, no sideways scrolling",
1629
+ ar: "\u0644\u0627 \u0634\u064A\u0621 \u0645\u0642\u0635\u0648\u0635 \u0623\u0648 \u0645\u062A\u062F\u0627\u062E\u0644\u060C \u0648\u0628\u062F\u0648\u0646 \u062A\u0645\u0631\u064A\u0631 \u062C\u0627\u0646\u0628\u064A"
1630
+ },
1631
+ risk: "amber"
1632
+ },
1633
+ {
1634
+ path: ANY_PAGE,
1635
+ what: { en: "Tap the small controls with a thumb", ar: "\u0627\u0636\u063A\u0637 \u0627\u0644\u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0635\u063A\u064A\u0631\u0629 \u0628\u0625\u0628\u0647\u0627\u0645\u0643" },
1636
+ expect: { en: "They are big enough to hit first time", ar: "\u0643\u0628\u064A\u0631\u0629 \u0628\u0645\u0627 \u064A\u0643\u0641\u064A \u0644\u0644\u0636\u063A\u0637 \u0645\u0646 \u0623\u0648\u0644 \u0645\u0631\u0629" },
1637
+ risk: "green"
1638
+ }
1639
+ ]
1640
+ }
1641
+ ];
1642
+
1521
1643
  // src/lib/storage.ts
1522
1644
  function isStorageAvailable() {
1523
1645
  if (typeof window === "undefined") return false;
@@ -1620,6 +1742,7 @@ var STR = {
1620
1742
  loc_hide: "Hide location",
1621
1743
  loc_locate: "Locate on page",
1622
1744
  journey_title: "Testing journey",
1745
+ journey_generic: "No test plan was set up for this project, so this is a general one that fits any app. Work through it, or ask whoever set up Qapture to add the real journey to qa.config.",
1623
1746
  export_name_title: "Name your export",
1624
1747
  export_name_placeholder: "file name",
1625
1748
  tap_element: "Tap an element",
@@ -1683,6 +1806,12 @@ var STR = {
1683
1806
  sync_lost_permission: "Folder access expired \u2014 click Reconnect to keep saving",
1684
1807
  sync_write_failed: "Couldn't write to the folder \u2014 notes are still saved in this browser",
1685
1808
  sync_unsupported: "Folder saving needs Chrome, Edge or Brave on desktop. Use Export here.",
1809
+ sync_zip_mode: "This browser cannot write into a folder directly, so Qapture builds the folder for you: name the project and campaign, and you get a zip that unzips into exactly that structure. It refreshes itself every few points.",
1810
+ sync_zip_start: "Start saving",
1811
+ sync_zip_save_now: "Save folder now",
1812
+ sync_zip_saved: "Saved {name} to your downloads",
1813
+ sync_zip_failed: "Couldn't build the folder \u2014 your notes are still safe in this browser",
1814
+ sync_zip_where: "Unzip into your QA folder:",
1686
1815
  sync_writing_to: "Writing to",
1687
1816
  storage_title: "Storage",
1688
1817
  storage_used: "{used} of {quota} used",
@@ -1807,6 +1936,7 @@ var STR = {
1807
1936
  loc_hide: "\u0625\u062E\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0642\u0639",
1808
1937
  loc_locate: "\u0625\u0638\u0647\u0627\u0631 \u0639\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0629",
1809
1938
  journey_title: "\u0631\u062D\u0644\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
1939
+ journey_generic: "\u0644\u0645 \u064A\u062A\u0645 \u0625\u0639\u062F\u0627\u062F \u062E\u0637\u0629 \u0627\u062E\u062A\u0628\u0627\u0631 \u0644\u0647\u0630\u0627 \u0627\u0644\u0645\u0634\u0631\u0648\u0639\u060C \u0644\u0630\u0644\u0643 \u0647\u0630\u0647 \u062E\u0637\u0629 \u0639\u0627\u0645\u0629 \u062A\u0635\u0644\u062D \u0644\u0623\u064A \u062A\u0637\u0628\u064A\u0642. \u0627\u0639\u0645\u0644 \u0628\u0647\u0627\u060C \u0623\u0648 \u0627\u0637\u0644\u0628 \u0645\u0645\u0646 \u062B\u0628\u0651\u062A Qapture \u0623\u0646 \u064A\u0636\u064A\u0641 \u0631\u062D\u0644\u0629 \u0627\u0644\u0645\u0634\u0631\u0648\u0639 \u0641\u064A qa.config.",
1810
1940
  export_name_title: "\u0633\u0645\u0650\u0651 \u0645\u0644\u0641 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
1811
1941
  export_name_placeholder: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641",
1812
1942
  tap_element: "\u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0639\u0646\u0635\u0631",
@@ -1870,6 +2000,12 @@ var STR = {
1870
2000
  sync_lost_permission: "\u0627\u0646\u062A\u0647\u062A \u0635\u0644\u0627\u062D\u064A\u0629 \u0627\u0644\u0648\u0635\u0648\u0644 \u0644\u0644\u0645\u062C\u0644\u062F \u2014 \u0627\u0636\u063A\u0637 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0631\u0628\u0637 \u0644\u0644\u0645\u062A\u0627\u0628\u0639\u0629",
1871
2001
  sync_write_failed: "\u062A\u0639\u0630\u0651\u0631\u062A \u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u0641\u064A \u0627\u0644\u0645\u062C\u0644\u062F \u2014 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A \u0645\u062D\u0641\u0648\u0638\u0629 \u0641\u064A \u0627\u0644\u0645\u062A\u0635\u0641\u062D",
1872
2002
  sync_unsupported: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F \u064A\u062A\u0637\u0644\u0628 Chrome \u0623\u0648 Edge \u0623\u0648 Brave \u0639\u0644\u0649 \u0633\u0637\u062D \u0627\u0644\u0645\u0643\u062A\u0628. \u0627\u0633\u062A\u062E\u062F\u0645 \u0627\u0644\u062A\u0635\u062F\u064A\u0631 \u0647\u0646\u0627.",
2003
+ sync_zip_mode: "\u0647\u0630\u0627 \u0627\u0644\u0645\u062A\u0635\u0641\u062D \u0644\u0627 \u064A\u0633\u062A\u0637\u064A\u0639 \u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u062F\u0627\u062E\u0644 \u0645\u062C\u0644\u062F \u0645\u0628\u0627\u0634\u0631\u0629\u060C \u0644\u0630\u0644\u0643 \u064A\u0628\u0646\u064A Qapture \u0627\u0644\u0645\u062C\u0644\u062F \u0646\u064A\u0627\u0628\u0629\u064B \u0639\u0646\u0643: \u0627\u0643\u062A\u0628 \u0627\u0633\u0645 \u0627\u0644\u0645\u0634\u0631\u0648\u0639 \u0648\u0627\u0644\u062D\u0645\u0644\u0629\u060C \u0648\u0633\u062A\u062D\u0635\u0644 \u0639\u0644\u0649 \u0645\u0644\u0641 \u0645\u0636\u063A\u0648\u0637 \u064A\u064F\u0641\u064E\u0643 \u0625\u0644\u0649 \u0627\u0644\u0628\u0646\u064A\u0629 \u0646\u0641\u0633\u0647\u0627 \u062A\u0645\u0627\u0645\u064B\u0627. \u0648\u064A\u062A\u062D\u062F\u0651\u062B \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627 \u0643\u0644 \u0628\u0636\u0639 \u0646\u0642\u0627\u0637.",
2004
+ sync_zip_start: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062D\u0641\u0638",
2005
+ sync_zip_save_now: "\u0627\u062D\u0641\u0638 \u0627\u0644\u0645\u062C\u0644\u062F \u0627\u0644\u0622\u0646",
2006
+ sync_zip_saved: "\u062A\u0645 \u062D\u0641\u0638 {name} \u0641\u064A \u0627\u0644\u062A\u0646\u0632\u064A\u0644\u0627\u062A",
2007
+ sync_zip_failed: "\u062A\u0639\u0630\u0651\u0631 \u0628\u0646\u0627\u0621 \u0627\u0644\u0645\u062C\u0644\u062F \u2014 \u0645\u0644\u0627\u062D\u0638\u0627\u062A\u0643 \u0645\u0627 \u0632\u0627\u0644\u062A \u0645\u062D\u0641\u0648\u0638\u0629 \u0641\u064A \u0647\u0630\u0627 \u0627\u0644\u0645\u062A\u0635\u0641\u062D",
2008
+ sync_zip_where: "\u0641\u064F\u0643\u0651 \u0627\u0644\u0636\u063A\u0637 \u062F\u0627\u062E\u0644 \u0645\u062C\u0644\u062F QA \u0644\u062F\u064A\u0643:",
1873
2009
  sync_writing_to: "\u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u0625\u0644\u0649",
1874
2010
  storage_title: "\u0627\u0644\u062A\u062E\u0632\u064A\u0646",
1875
2011
  storage_used: "{used} \u0645\u0646 {quota} \u0645\u0633\u062A\u062E\u062F\u064E\u0645\u0629",
@@ -2256,6 +2392,145 @@ async function grabExactRegion(rect) {
2256
2392
  }
2257
2393
  }
2258
2394
 
2395
+ // src/lib/cssColors.ts
2396
+ var UNSUPPORTED_FN = /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/i;
2397
+ function unsupportedFnScanner() {
2398
+ return /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/gi;
2399
+ }
2400
+ var COLOR_PROPS = [
2401
+ "color",
2402
+ "backgroundColor",
2403
+ "borderTopColor",
2404
+ "borderRightColor",
2405
+ "borderBottomColor",
2406
+ "borderLeftColor",
2407
+ "outlineColor",
2408
+ "textDecorationColor",
2409
+ "columnRuleColor",
2410
+ "caretColor",
2411
+ "fill",
2412
+ "stroke"
2413
+ ];
2414
+ var COMPOUND_PROPS = ["backgroundImage", "boxShadow", "textShadow"];
2415
+ var probeCtx;
2416
+ function getProbe() {
2417
+ if (probeCtx !== void 0) return probeCtx;
2418
+ try {
2419
+ const canvas = document.createElement("canvas");
2420
+ canvas.width = canvas.height = 1;
2421
+ probeCtx = canvas.getContext("2d", { willReadFrequently: true });
2422
+ } catch {
2423
+ probeCtx = null;
2424
+ }
2425
+ return probeCtx;
2426
+ }
2427
+ var conversionCache = /* @__PURE__ */ new Map();
2428
+ function toRenderableColor(value) {
2429
+ const ctx = getProbe();
2430
+ if (!ctx || !value) return null;
2431
+ const cached = conversionCache.get(value);
2432
+ if (cached !== void 0) return cached;
2433
+ const result = convertUncached(ctx, value);
2434
+ conversionCache.set(value, result);
2435
+ return result;
2436
+ }
2437
+ function convertUncached(ctx, value) {
2438
+ try {
2439
+ ctx.fillStyle = "#000000";
2440
+ ctx.fillStyle = value;
2441
+ const fromBlack = String(ctx.fillStyle);
2442
+ ctx.fillStyle = "#ffffff";
2443
+ ctx.fillStyle = value;
2444
+ if (fromBlack !== String(ctx.fillStyle)) return null;
2445
+ if (!UNSUPPORTED_FN.test(fromBlack)) return fromBlack;
2446
+ ctx.clearRect(0, 0, 1, 1);
2447
+ ctx.fillStyle = value;
2448
+ ctx.fillRect(0, 0, 1, 1);
2449
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
2450
+ return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
2451
+ } catch {
2452
+ return null;
2453
+ }
2454
+ }
2455
+ function neutralizeColorFunctions(value) {
2456
+ if (!value || !UNSUPPORTED_FN.test(value)) return value;
2457
+ const scanner = unsupportedFnScanner();
2458
+ let out = "";
2459
+ let last = 0;
2460
+ let match;
2461
+ while ((match = scanner.exec(value)) !== null) {
2462
+ const start = match.index;
2463
+ if (start < last) continue;
2464
+ let depth = 0;
2465
+ let i = start + match[0].length - 1;
2466
+ for (; i < value.length; i++) {
2467
+ if (value[i] === "(") depth++;
2468
+ else if (value[i] === ")") {
2469
+ depth--;
2470
+ if (depth === 0) break;
2471
+ }
2472
+ }
2473
+ if (i >= value.length) break;
2474
+ const converted = toRenderableColor(value.slice(start, i + 1));
2475
+ out += value.slice(last, start) + (converted ?? "rgba(0, 0, 0, 0)");
2476
+ last = i + 1;
2477
+ scanner.lastIndex = last;
2478
+ }
2479
+ return out + value.slice(last);
2480
+ }
2481
+ function hasUnsupportedColor(value) {
2482
+ return !!value && UNSUPPORTED_FN.test(value);
2483
+ }
2484
+ function safeBackgroundColor(value, fallback = "#ffffff") {
2485
+ if (!hasUnsupportedColor(value)) return value;
2486
+ return toRenderableColor(value) ?? fallback;
2487
+ }
2488
+ function neutralizeDocumentColors(doc, aggressive = false) {
2489
+ const view = doc.defaultView;
2490
+ if (!view || typeof view.getComputedStyle !== "function") return 0;
2491
+ let touched = 0;
2492
+ const all = doc.querySelectorAll("*");
2493
+ const elements = [doc.documentElement, ...Array.from(all)];
2494
+ for (const el of elements) {
2495
+ if (!el || !el.style) continue;
2496
+ let changed = false;
2497
+ let computed;
2498
+ try {
2499
+ computed = view.getComputedStyle(el);
2500
+ } catch {
2501
+ continue;
2502
+ }
2503
+ for (const prop of COLOR_PROPS) {
2504
+ const value = computed[prop] ?? computed[prop];
2505
+ if (!hasUnsupportedColor(value)) continue;
2506
+ const converted = toRenderableColor(value);
2507
+ if (converted) {
2508
+ el.style[prop] = converted;
2509
+ changed = true;
2510
+ }
2511
+ }
2512
+ for (const prop of COMPOUND_PROPS) {
2513
+ const value = computed[prop];
2514
+ if (!value || value === "none") continue;
2515
+ if (aggressive && prop !== "backgroundImage") {
2516
+ el.style[prop] = "none";
2517
+ changed = true;
2518
+ continue;
2519
+ }
2520
+ if (!hasUnsupportedColor(value)) continue;
2521
+ if (aggressive) {
2522
+ el.style[prop] = "none";
2523
+ changed = true;
2524
+ continue;
2525
+ }
2526
+ el.style[prop] = neutralizeColorFunctions(value);
2527
+ changed = true;
2528
+ }
2529
+ if (changed) touched++;
2530
+ }
2531
+ return touched;
2532
+ }
2533
+
2259
2534
  // src/lib/capture.ts
2260
2535
  var HTML2CANVAS_TIMEOUT_MS = 1e4;
2261
2536
  var FALLBACK_PAGE_BACKGROUND = "#ffffff";
@@ -2427,7 +2702,7 @@ function cropCanvas(full, rect, scale) {
2427
2702
  ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
2428
2703
  return out;
2429
2704
  }
2430
- async function captureViaDom(rect, sx, sy) {
2705
+ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2431
2706
  const { default: html2canvas } = await import('html2canvas');
2432
2707
  const scale = Math.min(window.devicePixelRatio || 1, 2);
2433
2708
  const vw = document.documentElement.clientWidth || window.innerWidth;
@@ -2445,14 +2720,19 @@ async function captureViaDom(rect, sx, sy) {
2445
2720
  useCORS: true,
2446
2721
  allowTaint: true,
2447
2722
  // The page's own background, never null — see resolvePageBackground().
2448
- backgroundColor: resolvePageBackground(),
2723
+ // Passed straight to html2canvas's parser, so it gets the same
2724
+ // CSS Color 4 treatment the document walk applies.
2725
+ backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
2449
2726
  logging: false,
2450
2727
  scrollX: sx,
2451
2728
  scrollY: sy,
2452
2729
  windowWidth: vw,
2453
2730
  windowHeight: vh,
2454
2731
  ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay"),
2455
- onclone: (doc) => pinStuckClones(doc)
2732
+ onclone: (doc) => {
2733
+ neutralizeDocumentColors(doc, aggressiveColors);
2734
+ pinStuckClones(doc);
2735
+ }
2456
2736
  }),
2457
2737
  HTML2CANVAS_TIMEOUT_MS
2458
2738
  );
@@ -2487,8 +2767,16 @@ async function captureRegion(rect, scroll, prefer = "auto") {
2487
2767
  const blob = await encodeShot(canvas);
2488
2768
  return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
2489
2769
  } catch (err) {
2490
- console.warn("[QA] region capture failed:", err);
2491
- return { status: "failed" };
2770
+ console.warn("[QA] region capture failed, retrying without gradients/shadows:", err);
2771
+ try {
2772
+ const canvas = await captureViaDom(rect, sx, sy, true);
2773
+ if (!canvas) return { status: "failed" };
2774
+ const blob = await encodeShot(canvas);
2775
+ return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
2776
+ } catch (retryErr) {
2777
+ console.warn("[QA] region capture failed after retry:", retryErr);
2778
+ return { status: "failed" };
2779
+ }
2492
2780
  }
2493
2781
  }
2494
2782
 
@@ -2911,6 +3199,7 @@ async function shareZipFile(blob, filename, title) {
2911
3199
  // src/lib/fsSync.ts
2912
3200
  var MAX_SLUG_LENGTH = 48;
2913
3201
  var FS_ROOT_META_KEY = "fsSyncRoot";
3202
+ var FS_CAMPAIGN_META_KEY = "fsSyncCampaign";
2914
3203
  var root = null;
2915
3204
  var campaignDir = null;
2916
3205
  var notesDir = null;
@@ -2919,6 +3208,7 @@ var campaign = null;
2919
3208
  var noteIndex = {};
2920
3209
  var state = "unsupported";
2921
3210
  var lastError = "";
3211
+ var metaStore = null;
2922
3212
  var bulkWriting = false;
2923
3213
  var listeners = /* @__PURE__ */ new Set();
2924
3214
  function setState(next, error = "") {
@@ -2941,8 +3231,20 @@ function onFsSyncChange(fn) {
2941
3231
  function isFsSyncSupported() {
2942
3232
  return typeof window !== "undefined" && typeof window.showDirectoryPicker === "function";
2943
3233
  }
3234
+ function isFsSyncAvailable() {
3235
+ return typeof window !== "undefined";
3236
+ }
3237
+ function getFsSyncEngine() {
3238
+ if (isFsSyncSupported() || typeof document === "undefined") return "disk";
3239
+ return "download";
3240
+ }
2944
3241
  function getFsSyncState() {
2945
- if (!isFsSyncSupported()) return "unsupported";
3242
+ if (!isFsSyncAvailable()) return "unsupported";
3243
+ if (!isFsSyncSupported()) {
3244
+ if (state === "syncing") return "syncing";
3245
+ if (state === "error") return "error";
3246
+ return "connected";
3247
+ }
2946
3248
  return state === "unsupported" ? "off" : state;
2947
3249
  }
2948
3250
  function getFsSyncError() {
@@ -2952,9 +3254,18 @@ function getFsSyncCampaign() {
2952
3254
  return campaign;
2953
3255
  }
2954
3256
  function getFsSyncPath() {
3257
+ if (getFsSyncEngine() === "download") return campaign ? campaignRelativePath() : "";
2955
3258
  if (!root) return "";
2956
3259
  if (!campaign) return root.name;
2957
- return `${root.name}/${safeSegment(campaign.project)}/${safeSegment(campaign.campaign)}`;
3260
+ return `${root.name}/${campaignRelativePath()}`;
3261
+ }
3262
+ function campaignRelativePath() {
3263
+ if (!campaign) return "";
3264
+ return `${safeSegment(campaign.project)}/${safeSegment(campaign.campaign)}`;
3265
+ }
3266
+ function campaignZipFileName() {
3267
+ if (!campaign) return "qa-campaign.zip";
3268
+ return `${safeSegment(campaign.project)} - ${safeSegment(campaign.campaign)}.zip`;
2958
3269
  }
2959
3270
  function safeSegment(input, fallback = "untitled") {
2960
3271
  let s = (input ?? "").normalize("NFC").replace(/[\\/:*?"<>|\u0000-\u001F]/g, "-");
@@ -2996,10 +3307,25 @@ async function permissionFor(handle, request) {
2996
3307
  return "granted";
2997
3308
  }
2998
3309
  async function restoreFsSync(store) {
2999
- if (!isFsSyncSupported()) {
3310
+ metaStore = store;
3311
+ if (!isFsSyncAvailable()) {
3000
3312
  setState("unsupported");
3001
3313
  return "unsupported";
3002
3314
  }
3315
+ if (getFsSyncEngine() === "download") {
3316
+ try {
3317
+ const saved = await store.getMeta(FS_CAMPAIGN_META_KEY);
3318
+ if (saved?.campaign?.project && saved.active !== false) {
3319
+ campaign = saved.campaign;
3320
+ noteIndex = saved.notes ?? {};
3321
+ setState("syncing");
3322
+ return "syncing";
3323
+ }
3324
+ } catch {
3325
+ }
3326
+ setState("connected");
3327
+ return "connected";
3328
+ }
3003
3329
  try {
3004
3330
  const saved = await store.getMeta(FS_ROOT_META_KEY);
3005
3331
  if (!saved || typeof saved.getDirectoryHandle !== "function") {
@@ -3016,7 +3342,11 @@ async function restoreFsSync(store) {
3016
3342
  }
3017
3343
  }
3018
3344
  async function chooseFsSyncRoot(store) {
3019
- if (!isFsSyncSupported()) return false;
3345
+ metaStore = store;
3346
+ if (!isFsSyncSupported()) {
3347
+ setState("connected");
3348
+ return true;
3349
+ }
3020
3350
  const picker = window.showDirectoryPicker;
3021
3351
  try {
3022
3352
  const handle = await picker({ id: "qapture-qa", mode: "readwrite", startIn: "documents" });
@@ -3064,6 +3394,30 @@ function describeError(err) {
3064
3394
  return e?.message || "write-failed";
3065
3395
  }
3066
3396
  async function openFsCampaign(input) {
3397
+ if (getFsSyncEngine() === "download") {
3398
+ const sameAsBefore = campaign?.project === input.project && campaign?.campaign === input.campaign;
3399
+ let startedAt = sameAsBefore && campaign ? campaign.startedAt : (/* @__PURE__ */ new Date()).toISOString();
3400
+ if (!sameAsBefore) {
3401
+ noteIndex = {};
3402
+ try {
3403
+ const saved = await metaStore?.getMeta(FS_CAMPAIGN_META_KEY);
3404
+ if (saved?.campaign?.project === input.project && saved.campaign.campaign === input.campaign) {
3405
+ noteIndex = saved.notes ?? {};
3406
+ startedAt = saved.campaign.startedAt || startedAt;
3407
+ }
3408
+ } catch {
3409
+ }
3410
+ }
3411
+ campaign = {
3412
+ project: input.project,
3413
+ campaign: input.campaign,
3414
+ tester: input.tester,
3415
+ startedAt
3416
+ };
3417
+ await persistDownloadCampaign();
3418
+ setState("syncing");
3419
+ return true;
3420
+ }
3067
3421
  if (!root) return false;
3068
3422
  try {
3069
3423
  const perm = await permissionFor(root, false);
@@ -3102,11 +3456,23 @@ async function openFsCampaign(input) {
3102
3456
  }
3103
3457
  }
3104
3458
  function closeFsCampaign() {
3459
+ if (getFsSyncEngine() === "download") void persistDownloadCampaign(false);
3105
3460
  campaignDir = notesDir = shotsDir = null;
3106
3461
  campaign = null;
3107
3462
  noteIndex = {};
3463
+ if (getFsSyncEngine() === "download") {
3464
+ setState("connected");
3465
+ return;
3466
+ }
3108
3467
  if (root) setState("connected");
3109
3468
  }
3469
+ async function persistDownloadCampaign(active = true) {
3470
+ if (!metaStore || !campaign) return;
3471
+ try {
3472
+ await metaStore.setMeta(FS_CAMPAIGN_META_KEY, { campaign, notes: noteIndex, active });
3473
+ } catch {
3474
+ }
3475
+ }
3110
3476
  async function writeFile(dir, name, data) {
3111
3477
  const handle = await dir.getFileHandle(name, { create: true });
3112
3478
  const writable = await handle.createWritable();
@@ -3137,6 +3503,13 @@ function entryFor(note) {
3137
3503
  };
3138
3504
  }
3139
3505
  async function syncNoteToDisk(note, allNotes) {
3506
+ if (getFsSyncEngine() === "download") {
3507
+ if (!campaign) return false;
3508
+ noteIndex[note.id] = entryFor(note);
3509
+ await persistDownloadCampaign();
3510
+ setState("syncing");
3511
+ return true;
3512
+ }
3140
3513
  if (!campaignDir || !notesDir || !shotsDir || !campaign) return false;
3141
3514
  try {
3142
3515
  const previous = noteIndex[note.id];
@@ -3164,6 +3537,12 @@ async function syncNoteToDisk(note, allNotes) {
3164
3537
  }
3165
3538
  }
3166
3539
  async function removeNoteFromDisk(noteId, allNotes) {
3540
+ if (getFsSyncEngine() === "download") {
3541
+ if (!noteIndex[noteId]) return true;
3542
+ delete noteIndex[noteId];
3543
+ await persistDownloadCampaign();
3544
+ return true;
3545
+ }
3167
3546
  if (!campaignDir || !notesDir || !shotsDir) return false;
3168
3547
  const entry = noteIndex[noteId];
3169
3548
  if (!entry) return true;
@@ -3189,8 +3568,15 @@ function noteMarkdownForDisk(note, entry) {
3189
3568
  }
3190
3569
  async function flushCampaignFiles(allNotes) {
3191
3570
  if (!campaignDir || !campaign) return;
3192
- const known = allNotes.filter((n) => noteIndex[n.id]);
3193
- const ordered = known.slice().sort((a, b) => (noteIndex[a.id].seq ?? 0) - (noteIndex[b.id].seq ?? 0));
3571
+ await writeFile(campaignDir, "campaign.json", campaignJsonText(allNotes));
3572
+ await writeFile(campaignDir, "REPORT.md", reportMarkdownText(allNotes));
3573
+ }
3574
+ function orderedKnownNotes(allNotes) {
3575
+ return allNotes.filter((n) => noteIndex[n.id]).slice().sort((a, b) => (noteIndex[a.id].seq ?? 0) - (noteIndex[b.id].seq ?? 0));
3576
+ }
3577
+ function campaignJsonText(allNotes) {
3578
+ if (!campaign) return "{}";
3579
+ const ordered = orderedKnownNotes(allNotes);
3194
3580
  const meta = {
3195
3581
  project: campaign.project,
3196
3582
  campaign: campaign.campaign,
@@ -3200,7 +3586,11 @@ async function flushCampaignFiles(allNotes) {
3200
3586
  noteCount: ordered.length,
3201
3587
  notes: noteIndex
3202
3588
  };
3203
- await writeFile(campaignDir, "campaign.json", JSON.stringify(meta, null, 2));
3589
+ return JSON.stringify(meta, null, 2);
3590
+ }
3591
+ function reportMarkdownText(allNotes) {
3592
+ if (!campaign) return "";
3593
+ const ordered = orderedKnownNotes(allNotes);
3204
3594
  const counts = { bug: 0, question: 0, polish: 0, verified: 0 };
3205
3595
  for (const n of ordered) {
3206
3596
  const sev = n.severity ?? "bug";
@@ -3214,20 +3604,66 @@ async function flushCampaignFiles(allNotes) {
3214
3604
  "",
3215
3605
  campaign.tester ? `Tester: ${campaign.tester} ` : "",
3216
3606
  `Started: ${campaign.startedAt} `,
3217
- `Updated: ${meta.updatedAt} `,
3607
+ `Updated: ${(/* @__PURE__ */ new Date()).toISOString()} `,
3218
3608
  `Points: ${ordered.length} (${counts.bug} bug, ${counts.question} question, ${counts.polish} polish \xB7 ${counts.verified} verified)`,
3219
3609
  "",
3220
- "Written live by Qapture as each point was saved. Screenshots are in",
3610
+ getFsSyncEngine() === "download" ? "Saved by Qapture as the campaign was tested. Screenshots are in" : "Written live by Qapture as each point was saved. Screenshots are in",
3221
3611
  "`screenshots/`, one Markdown file per point in `notes/`.",
3222
3612
  "",
3223
3613
  "---",
3224
3614
  ""
3225
3615
  ].filter((line) => line !== "").join("\n");
3226
3616
  const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
3227
- await writeFile(campaignDir, "REPORT.md", `${header}
3228
- ${body}`);
3617
+ return `${header}
3618
+ ${body}`;
3619
+ }
3620
+ async function buildCampaignZipBlob(allNotes) {
3621
+ if (!campaign || typeof document === "undefined") return null;
3622
+ try {
3623
+ const { default: JSZip } = await import('jszip');
3624
+ const zip = new JSZip();
3625
+ const base = zip.folder(safeSegment(campaign.project, "Project"))?.folder(safeSegment(campaign.campaign, defaultCampaignName()));
3626
+ if (!base) return null;
3627
+ base.file("campaign.json", campaignJsonText(allNotes));
3628
+ base.file("REPORT.md", reportMarkdownText(allNotes));
3629
+ const notesFolder = base.folder("notes");
3630
+ const shotsFolder = base.folder("screenshots");
3631
+ for (const note of orderedKnownNotes(allNotes)) {
3632
+ const entry = noteIndex[note.id];
3633
+ notesFolder?.file(entry.file, noteMarkdownForDisk(note, entry));
3634
+ if (note.screenshot && entry.shot) shotsFolder?.file(entry.shot, note.screenshot);
3635
+ if (note.afterScreenshot && entry.after) shotsFolder?.file(entry.after, note.afterScreenshot);
3636
+ }
3637
+ return await zip.generateAsync({ type: "blob" });
3638
+ } catch (err) {
3639
+ setState("error", describeError(err));
3640
+ return null;
3641
+ }
3642
+ }
3643
+ async function downloadCampaignZip(allNotes) {
3644
+ const blob = await buildCampaignZipBlob(allNotes);
3645
+ if (!blob) return false;
3646
+ const url = URL.createObjectURL(blob);
3647
+ const a = document.createElement("a");
3648
+ a.href = url;
3649
+ a.download = campaignZipFileName();
3650
+ document.body.appendChild(a);
3651
+ a.click();
3652
+ a.remove();
3653
+ setTimeout(() => URL.revokeObjectURL(url), 2e3);
3654
+ setState("syncing");
3655
+ return true;
3229
3656
  }
3230
3657
  async function syncAllToDisk(allNotes) {
3658
+ if (getFsSyncEngine() === "download") {
3659
+ if (!campaign) return false;
3660
+ for (const note of allNotes.slice().reverse()) {
3661
+ if (!noteIndex[note.id]) noteIndex[note.id] = entryFor(note);
3662
+ }
3663
+ await persistDownloadCampaign();
3664
+ setState("syncing");
3665
+ return true;
3666
+ }
3231
3667
  if (!campaignDir) return false;
3232
3668
  const ordered = allNotes.slice().reverse();
3233
3669
  bulkWriting = true;
@@ -4023,6 +4459,15 @@ function QaProvider({
4023
4459
  closeFsCampaign();
4024
4460
  notify(t("sync_paused"), { id: "sync-state" });
4025
4461
  }, [notify, t]);
4462
+ const saveSyncFolderNow = React.useCallback(async () => {
4463
+ const name = campaignZipFileName();
4464
+ const ok = await downloadCampaignZip(notesRef.current);
4465
+ notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
4466
+ tone: ok ? "success" : "error",
4467
+ id: "sync-zip"
4468
+ });
4469
+ return ok;
4470
+ }, [notify, t]);
4026
4471
  const forgetSyncFolder = React.useCallback(async () => {
4027
4472
  await disconnectFsSync(idb);
4028
4473
  storage.setJSON(LAST_CAMPAIGN_KEY, {});
@@ -4071,10 +4516,23 @@ function QaProvider({
4071
4516
  React.useEffect(() => {
4072
4517
  if (!autoBackup) return;
4073
4518
  if (notes.length === 0 || isExporting) return;
4074
- if (getFsSyncState() === "syncing") return;
4519
+ const syncing = getFsSyncState() === "syncing";
4520
+ const viaDownload = getFsSyncEngine() === "download";
4521
+ if (syncing && !viaDownload) return;
4075
4522
  const lastAt = Number(storage.getItem(AUTO_BACKUP_AT_KEY) ?? "0") || 0;
4076
4523
  if (notes.length < lastAt + AUTO_BACKUP_EVERY) return;
4077
4524
  storage.setItem(AUTO_BACKUP_AT_KEY, String(notes.length));
4525
+ if (syncing && viaDownload) {
4526
+ const name = campaignZipFileName();
4527
+ void downloadCampaignZip(notesRef.current).then((ok) => {
4528
+ notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
4529
+ tone: ok ? "success" : "error",
4530
+ id: "sync-zip",
4531
+ duration: 3e3
4532
+ });
4533
+ });
4534
+ return;
4535
+ }
4078
4536
  const stamp = nowIso().slice(0, 16).replace(/[:T]/g, "-");
4079
4537
  void exportZipRef.current(`qa-autosave-${stamp}-${notes.length}`).then(() => {
4080
4538
  notify(t("autosave_done", { n: notes.length }), { id: "autosave", duration: 3e3 });
@@ -4375,6 +4833,7 @@ function QaProvider({
4375
4833
  }, []);
4376
4834
  React.useEffect(() => {
4377
4835
  if (typeof window === "undefined") return;
4836
+ if (notesLoading) return;
4378
4837
  const params = new URLSearchParams(window.location.search);
4379
4838
  const raw = params.get("qa");
4380
4839
  if (!raw || !raw.startsWith("walk")) return;
@@ -4393,7 +4852,7 @@ function QaProvider({
4393
4852
  } else {
4394
4853
  startWalk("plan", 0);
4395
4854
  }
4396
- }, [startWalk]);
4855
+ }, [startWalk, notesLoading]);
4397
4856
  const value = {
4398
4857
  // Data
4399
4858
  notes,
@@ -4414,7 +4873,10 @@ function QaProvider({
4414
4873
  brand: config.brand,
4415
4874
  loginField: config.loginField,
4416
4875
  credentials: config.credentials,
4417
- journey: config.journey,
4876
+ // Never the raw config value: an unconfigured project falls back to the
4877
+ // generic plan so the Guide is never a blank page.
4878
+ journey: journeyOrEmpty(config.journey),
4879
+ journeyIsGeneric: !config.journey?.some((lane) => lane.steps.length > 0),
4418
4880
  preamble: config.preamble,
4419
4881
  // i18n helpers
4420
4882
  t,
@@ -4462,7 +4924,8 @@ function QaProvider({
4462
4924
  // rebuilt when fsSync's module state changes (path/campaign live outside
4463
4925
  // React).
4464
4926
  sync: {
4465
- supported: isFsSyncSupported(),
4927
+ supported: isFsSyncAvailable(),
4928
+ engine: getFsSyncEngine(),
4466
4929
  state: syncState,
4467
4930
  path: syncTick >= 0 ? getFsSyncPath() : "",
4468
4931
  campaign: getFsSyncCampaign(),
@@ -4472,6 +4935,7 @@ function QaProvider({
4472
4935
  reconnectSyncFolder,
4473
4936
  startSyncCampaign,
4474
4937
  stopSyncCampaign,
4938
+ saveSyncFolderNow,
4475
4939
  forgetSyncFolder,
4476
4940
  suggestCampaignName,
4477
4941
  lastCampaign,
@@ -4516,7 +4980,9 @@ function QaProvider({
4516
4980
  return /* @__PURE__ */ jsxRuntime.jsx(QaContext.Provider, { value, children });
4517
4981
  }
4518
4982
  function journeyOrEmpty(journey) {
4519
- return Array.isArray(journey) ? journey : [];
4983
+ const configured = Array.isArray(journey) ? journey : [];
4984
+ const hasSteps = configured.some((lane) => lane.steps.length > 0);
4985
+ return hasSteps ? configured : FALLBACK_JOURNEY;
4520
4986
  }
4521
4987
  function useQa() {
4522
4988
  const ctx = React.useContext(QaContext);
@@ -6497,6 +6963,7 @@ function SettingsSheet({ onClose }) {
6497
6963
  forgetSyncFolder,
6498
6964
  suggestCampaignName,
6499
6965
  lastCampaign,
6966
+ saveSyncFolderNow,
6500
6967
  storageHealth,
6501
6968
  refreshStorageHealth,
6502
6969
  requestPersistentStorage: requestPersistentStorage2,
@@ -6538,6 +7005,7 @@ function SettingsSheet({ onClose }) {
6538
7005
  }
6539
7006
  };
6540
7007
  const syncing = sync.state === "syncing";
7008
+ const viaZip = sync.engine === "download";
6541
7009
  const quotaKnown = storageHealth.supported && storageHealth.quotaBytes > 0;
6542
7010
  const usedPct = quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
6543
7011
  const meterColor = storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
@@ -6565,6 +7033,7 @@ function SettingsSheet({ onClose }) {
6565
7033
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex-1 qa-space-y-4 qa-overflow-y-auto qa-p-3", children: [
6566
7034
  /* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Folder", title: t("sync_title"), children: [
6567
7035
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("sync_hint") }),
7036
+ viaZip ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-rounded-lg qa-border qa-border-dashed qa-border-subtle qa-p-2 qa-text-10 qa-text-mid qa-leading-relaxed", children: t("sync_zip_mode") }) : null,
6568
7037
  !sync.supported ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-rounded-lg qa-border qa-border-dashed qa-border-subtle qa-p-2 qa-text-10 qa-text-mid", children: t("sync_unsupported") }) : sync.state === "off" ? /* @__PURE__ */ jsxRuntime.jsxs(
6569
7038
  "button",
6570
7039
  {
@@ -6592,24 +7061,41 @@ function SettingsSheet({ onClose }) {
6592
7061
  ]
6593
7062
  }
6594
7063
  ) : syncing ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", children: [
7064
+ viaZip && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo", children: t("sync_zip_where") }),
6595
7065
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-rounded-lg qa-bg-success-tint qa-text-success qa-px-2 qa-py-1.5 qa-text-10", children: [
6596
7066
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "FolderCheck", size: 13, className: "qa-shrink-0" }),
6597
7067
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", title: sync.path, children: sync.path })
6598
7068
  ] }),
6599
- /* @__PURE__ */ jsxRuntime.jsx(
6600
- "button",
6601
- {
6602
- type: "button",
6603
- onClick: stopSyncCampaign,
6604
- className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
6605
- style: { background: "transparent", cursor: "pointer" },
6606
- children: t("sync_stop")
6607
- }
6608
- )
7069
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-flex-wrap qa-gap-2", children: [
7070
+ viaZip && /* @__PURE__ */ jsxRuntime.jsxs(
7071
+ "button",
7072
+ {
7073
+ type: "button",
7074
+ disabled: busy,
7075
+ onClick: () => void run2(saveSyncFolderNow),
7076
+ className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-lg qa-bg-accent qa-px-3 qa-py-1.5 qa-text-xs qa-font-semibold",
7077
+ style: { border: "none", cursor: "pointer" },
7078
+ children: [
7079
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Download", size: 13 }),
7080
+ t("sync_zip_save_now")
7081
+ ]
7082
+ }
7083
+ ),
7084
+ /* @__PURE__ */ jsxRuntime.jsx(
7085
+ "button",
7086
+ {
7087
+ type: "button",
7088
+ onClick: stopSyncCampaign,
7089
+ className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
7090
+ style: { background: "transparent", cursor: "pointer" },
7091
+ children: t("sync_stop")
7092
+ }
7093
+ )
7094
+ ] })
6609
7095
  ] }) : (
6610
7096
  /* connected: folder chosen, campaign not open yet */
6611
7097
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", children: [
6612
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
7098
+ !viaZip && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
6613
7099
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Folder", size: 12, className: "qa-shrink-0" }),
6614
7100
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", children: sync.path })
6615
7101
  ] }),
@@ -6631,11 +7117,11 @@ function SettingsSheet({ onClose }) {
6631
7117
  style: { border: "none", cursor: "pointer" },
6632
7118
  children: [
6633
7119
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 13 }),
6634
- t("sync_start")
7120
+ viaZip ? t("sync_zip_start") : t("sync_start")
6635
7121
  ]
6636
7122
  }
6637
7123
  ),
6638
- /* @__PURE__ */ jsxRuntime.jsx(
7124
+ !viaZip && /* @__PURE__ */ jsxRuntime.jsx(
6639
7125
  "button",
6640
7126
  {
6641
7127
  type: "button",
@@ -7145,12 +7631,31 @@ function Lane({
7145
7631
  ] });
7146
7632
  }
7147
7633
  function GuideSection() {
7148
- const { guideChecked, toggleGuide, t, journey, pick: pick2, lang, startTestAlong, startWalk, testAlongSteps } = useQa();
7634
+ const {
7635
+ guideChecked,
7636
+ toggleGuide,
7637
+ t,
7638
+ journey,
7639
+ journeyIsGeneric,
7640
+ pick: pick2,
7641
+ lang,
7642
+ startTestAlong,
7643
+ startWalk,
7644
+ testAlongSteps
7645
+ } = useQa();
7149
7646
  const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
7150
7647
  const done = all.filter((k) => guideChecked.has(k)).length;
7151
7648
  const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
7152
7649
  const coverage = computeCoverage(journey, guideChecked);
7153
7650
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-3", children: [
7651
+ journeyIsGeneric && /* @__PURE__ */ jsxRuntime.jsx(
7652
+ "p",
7653
+ {
7654
+ className: "qa-m-0 qa-rounded-xl qa-border qa-border-dashed qa-border-subtle qa-p-2 qa-text-10 qa-text-mid qa-leading-relaxed",
7655
+ "data-qa-generic-plan": "true",
7656
+ children: t("journey_generic")
7657
+ }
7658
+ ),
7154
7659
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
7155
7660
  coverage.red.total > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-1.5 qa-text-11", children: [
7156
7661
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-bg-danger-tint qa-text-danger qa-rounded qa-px-1 qa-font-bold", children: lang === "ar" ? "\u0623\u062D\u0645\u0631" : "RED" }),
@@ -7207,8 +7712,7 @@ function GuideSection() {
7207
7712
  pick: pick2
7208
7713
  },
7209
7714
  g.id
7210
- )),
7211
- journey.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-py-8 qa-text-center qa-text-sm qa-text-slate-400", children: t("tab_guide") })
7715
+ ))
7212
7716
  ] });
7213
7717
  }
7214
7718
  var TABS = [
@@ -8053,7 +8557,7 @@ function WalkHud() {
8053
8557
  )
8054
8558
  ] });
8055
8559
  }
8056
- const onThisPage = typeof window !== "undefined" && stop.path === window.location.pathname;
8560
+ const onThisPage = stop.path === ANY_PAGE || typeof window !== "undefined" && stop.path === window.location.pathname;
8057
8561
  return /* @__PURE__ */ jsxRuntime.jsxs(
8058
8562
  "div",
8059
8563
  {
@@ -9151,6 +9655,7 @@ function CaptureMode() {
9151
9655
  type: "button",
9152
9656
  onClick: () => selection && void runCapture(selection.rect),
9153
9657
  className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-mid qa-focus-ring",
9658
+ style: { background: "transparent", cursor: "pointer" },
9154
9659
  children: [
9155
9660
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 13 }),
9156
9661
  t("retry")
@@ -9452,5 +9957,5 @@ function Qapture({ config }) {
9452
9957
  exports.Qapture = Qapture;
9453
9958
  exports.deleteQaDatabase = deleteQaDatabase;
9454
9959
  exports.initQaStudio = initQaStudio;
9455
- //# sourceMappingURL=chunk-UOUFJCPF.cjs.map
9456
- //# sourceMappingURL=chunk-UOUFJCPF.cjs.map
9960
+ //# sourceMappingURL=chunk-WAOYPLZA.cjs.map
9961
+ //# sourceMappingURL=chunk-WAOYPLZA.cjs.map