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.
@@ -1511,6 +1511,128 @@ function matchRouteToSteps(journey, route) {
1511
1511
  return [...exact, ...param];
1512
1512
  }
1513
1513
 
1514
+ // src/lib/fallbackJourney.ts
1515
+ var ANY_PAGE = "*";
1516
+ var FALLBACK_JOURNEY = [
1517
+ {
1518
+ id: "first-run",
1519
+ role: { en: "First look", ar: "\u0627\u0644\u0646\u0638\u0631\u0629 \u0627\u0644\u0623\u0648\u0644\u0649" },
1520
+ steps: [
1521
+ {
1522
+ path: ANY_PAGE,
1523
+ 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" },
1524
+ expect: {
1525
+ en: "Content appears, nothing flashes or jumps, no error in the corner",
1526
+ 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"
1527
+ },
1528
+ risk: "red"
1529
+ },
1530
+ {
1531
+ path: ANY_PAGE,
1532
+ 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" },
1533
+ expect: {
1534
+ en: "It is obvious what this is and what to do next",
1535
+ 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"
1536
+ },
1537
+ risk: "amber"
1538
+ }
1539
+ ]
1540
+ },
1541
+ {
1542
+ id: "moving-around",
1543
+ role: { en: "Moving around", ar: "\u0627\u0644\u062A\u0646\u0642\u0644" },
1544
+ steps: [
1545
+ {
1546
+ path: ANY_PAGE,
1547
+ 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" },
1548
+ 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" },
1549
+ risk: "red"
1550
+ },
1551
+ {
1552
+ path: ANY_PAGE,
1553
+ 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" },
1554
+ 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" },
1555
+ risk: "amber"
1556
+ },
1557
+ {
1558
+ path: ANY_PAGE,
1559
+ 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" },
1560
+ 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" },
1561
+ risk: "red"
1562
+ }
1563
+ ]
1564
+ },
1565
+ {
1566
+ id: "the-main-thing",
1567
+ role: { en: "The main thing", ar: "\u0627\u0644\u0645\u0647\u0645\u0629 \u0627\u0644\u0623\u0633\u0627\u0633\u064A\u0629" },
1568
+ steps: [
1569
+ {
1570
+ path: ANY_PAGE,
1571
+ what: {
1572
+ en: "Do what this app is for, start to finish, without shortcuts",
1573
+ 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"
1574
+ },
1575
+ 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" },
1576
+ risk: "red"
1577
+ },
1578
+ {
1579
+ path: ANY_PAGE,
1580
+ 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" },
1581
+ 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" },
1582
+ risk: "amber"
1583
+ }
1584
+ ]
1585
+ },
1586
+ {
1587
+ id: "when-it-goes-wrong",
1588
+ role: { en: "When it goes wrong", ar: "\u0639\u0646\u062F \u0627\u0644\u062E\u0637\u0623" },
1589
+ steps: [
1590
+ {
1591
+ path: ANY_PAGE,
1592
+ 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" },
1593
+ expect: {
1594
+ en: "It refuses politely and says which field and why \u2014 it does not crash or save",
1595
+ 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"
1596
+ },
1597
+ risk: "red"
1598
+ },
1599
+ {
1600
+ path: ANY_PAGE,
1601
+ 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" },
1602
+ 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" },
1603
+ risk: "amber"
1604
+ },
1605
+ {
1606
+ path: ANY_PAGE,
1607
+ 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" },
1608
+ 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" },
1609
+ risk: "red"
1610
+ }
1611
+ ]
1612
+ },
1613
+ {
1614
+ id: "on-a-phone",
1615
+ role: { en: "On a phone", ar: "\u0639\u0644\u0649 \u0627\u0644\u0647\u0627\u062A\u0641" },
1616
+ steps: [
1617
+ {
1618
+ path: ANY_PAGE,
1619
+ 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" },
1620
+ expect: {
1621
+ en: "Nothing is cut off, nothing overlaps, no sideways scrolling",
1622
+ 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"
1623
+ },
1624
+ risk: "amber"
1625
+ },
1626
+ {
1627
+ path: ANY_PAGE,
1628
+ 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" },
1629
+ 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" },
1630
+ risk: "green"
1631
+ }
1632
+ ]
1633
+ }
1634
+ ];
1635
+
1514
1636
  // src/lib/storage.ts
1515
1637
  function isStorageAvailable() {
1516
1638
  if (typeof window === "undefined") return false;
@@ -1613,6 +1735,7 @@ var STR = {
1613
1735
  loc_hide: "Hide location",
1614
1736
  loc_locate: "Locate on page",
1615
1737
  journey_title: "Testing journey",
1738
+ 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.",
1616
1739
  export_name_title: "Name your export",
1617
1740
  export_name_placeholder: "file name",
1618
1741
  tap_element: "Tap an element",
@@ -1676,6 +1799,12 @@ var STR = {
1676
1799
  sync_lost_permission: "Folder access expired \u2014 click Reconnect to keep saving",
1677
1800
  sync_write_failed: "Couldn't write to the folder \u2014 notes are still saved in this browser",
1678
1801
  sync_unsupported: "Folder saving needs Chrome, Edge or Brave on desktop. Use Export here.",
1802
+ 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.",
1803
+ sync_zip_start: "Start saving",
1804
+ sync_zip_save_now: "Save folder now",
1805
+ sync_zip_saved: "Saved {name} to your downloads",
1806
+ sync_zip_failed: "Couldn't build the folder \u2014 your notes are still safe in this browser",
1807
+ sync_zip_where: "Unzip into your QA folder:",
1679
1808
  sync_writing_to: "Writing to",
1680
1809
  storage_title: "Storage",
1681
1810
  storage_used: "{used} of {quota} used",
@@ -1800,6 +1929,7 @@ var STR = {
1800
1929
  loc_hide: "\u0625\u062E\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0642\u0639",
1801
1930
  loc_locate: "\u0625\u0638\u0647\u0627\u0631 \u0639\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0629",
1802
1931
  journey_title: "\u0631\u062D\u0644\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
1932
+ 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.",
1803
1933
  export_name_title: "\u0633\u0645\u0650\u0651 \u0645\u0644\u0641 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
1804
1934
  export_name_placeholder: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641",
1805
1935
  tap_element: "\u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0639\u0646\u0635\u0631",
@@ -1863,6 +1993,12 @@ var STR = {
1863
1993
  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",
1864
1994
  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",
1865
1995
  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.",
1996
+ 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.",
1997
+ sync_zip_start: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062D\u0641\u0638",
1998
+ sync_zip_save_now: "\u0627\u062D\u0641\u0638 \u0627\u0644\u0645\u062C\u0644\u062F \u0627\u0644\u0622\u0646",
1999
+ sync_zip_saved: "\u062A\u0645 \u062D\u0641\u0638 {name} \u0641\u064A \u0627\u0644\u062A\u0646\u0632\u064A\u0644\u0627\u062A",
2000
+ 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",
2001
+ 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:",
1866
2002
  sync_writing_to: "\u0627\u0644\u0643\u062A\u0627\u0628\u0629 \u0625\u0644\u0649",
1867
2003
  storage_title: "\u0627\u0644\u062A\u062E\u0632\u064A\u0646",
1868
2004
  storage_used: "{used} \u0645\u0646 {quota} \u0645\u0633\u062A\u062E\u062F\u064E\u0645\u0629",
@@ -2249,6 +2385,145 @@ async function grabExactRegion(rect) {
2249
2385
  }
2250
2386
  }
2251
2387
 
2388
+ // src/lib/cssColors.ts
2389
+ var UNSUPPORTED_FN = /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/i;
2390
+ function unsupportedFnScanner() {
2391
+ return /\b(?:oklch|oklab|lch|lab|color-mix|color)\(/gi;
2392
+ }
2393
+ var COLOR_PROPS = [
2394
+ "color",
2395
+ "backgroundColor",
2396
+ "borderTopColor",
2397
+ "borderRightColor",
2398
+ "borderBottomColor",
2399
+ "borderLeftColor",
2400
+ "outlineColor",
2401
+ "textDecorationColor",
2402
+ "columnRuleColor",
2403
+ "caretColor",
2404
+ "fill",
2405
+ "stroke"
2406
+ ];
2407
+ var COMPOUND_PROPS = ["backgroundImage", "boxShadow", "textShadow"];
2408
+ var probeCtx;
2409
+ function getProbe() {
2410
+ if (probeCtx !== void 0) return probeCtx;
2411
+ try {
2412
+ const canvas = document.createElement("canvas");
2413
+ canvas.width = canvas.height = 1;
2414
+ probeCtx = canvas.getContext("2d", { willReadFrequently: true });
2415
+ } catch {
2416
+ probeCtx = null;
2417
+ }
2418
+ return probeCtx;
2419
+ }
2420
+ var conversionCache = /* @__PURE__ */ new Map();
2421
+ function toRenderableColor(value) {
2422
+ const ctx = getProbe();
2423
+ if (!ctx || !value) return null;
2424
+ const cached = conversionCache.get(value);
2425
+ if (cached !== void 0) return cached;
2426
+ const result = convertUncached(ctx, value);
2427
+ conversionCache.set(value, result);
2428
+ return result;
2429
+ }
2430
+ function convertUncached(ctx, value) {
2431
+ try {
2432
+ ctx.fillStyle = "#000000";
2433
+ ctx.fillStyle = value;
2434
+ const fromBlack = String(ctx.fillStyle);
2435
+ ctx.fillStyle = "#ffffff";
2436
+ ctx.fillStyle = value;
2437
+ if (fromBlack !== String(ctx.fillStyle)) return null;
2438
+ if (!UNSUPPORTED_FN.test(fromBlack)) return fromBlack;
2439
+ ctx.clearRect(0, 0, 1, 1);
2440
+ ctx.fillStyle = value;
2441
+ ctx.fillRect(0, 0, 1, 1);
2442
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
2443
+ return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
2444
+ } catch {
2445
+ return null;
2446
+ }
2447
+ }
2448
+ function neutralizeColorFunctions(value) {
2449
+ if (!value || !UNSUPPORTED_FN.test(value)) return value;
2450
+ const scanner = unsupportedFnScanner();
2451
+ let out = "";
2452
+ let last = 0;
2453
+ let match;
2454
+ while ((match = scanner.exec(value)) !== null) {
2455
+ const start = match.index;
2456
+ if (start < last) continue;
2457
+ let depth = 0;
2458
+ let i = start + match[0].length - 1;
2459
+ for (; i < value.length; i++) {
2460
+ if (value[i] === "(") depth++;
2461
+ else if (value[i] === ")") {
2462
+ depth--;
2463
+ if (depth === 0) break;
2464
+ }
2465
+ }
2466
+ if (i >= value.length) break;
2467
+ const converted = toRenderableColor(value.slice(start, i + 1));
2468
+ out += value.slice(last, start) + (converted ?? "rgba(0, 0, 0, 0)");
2469
+ last = i + 1;
2470
+ scanner.lastIndex = last;
2471
+ }
2472
+ return out + value.slice(last);
2473
+ }
2474
+ function hasUnsupportedColor(value) {
2475
+ return !!value && UNSUPPORTED_FN.test(value);
2476
+ }
2477
+ function safeBackgroundColor(value, fallback = "#ffffff") {
2478
+ if (!hasUnsupportedColor(value)) return value;
2479
+ return toRenderableColor(value) ?? fallback;
2480
+ }
2481
+ function neutralizeDocumentColors(doc, aggressive = false) {
2482
+ const view = doc.defaultView;
2483
+ if (!view || typeof view.getComputedStyle !== "function") return 0;
2484
+ let touched = 0;
2485
+ const all = doc.querySelectorAll("*");
2486
+ const elements = [doc.documentElement, ...Array.from(all)];
2487
+ for (const el of elements) {
2488
+ if (!el || !el.style) continue;
2489
+ let changed = false;
2490
+ let computed;
2491
+ try {
2492
+ computed = view.getComputedStyle(el);
2493
+ } catch {
2494
+ continue;
2495
+ }
2496
+ for (const prop of COLOR_PROPS) {
2497
+ const value = computed[prop] ?? computed[prop];
2498
+ if (!hasUnsupportedColor(value)) continue;
2499
+ const converted = toRenderableColor(value);
2500
+ if (converted) {
2501
+ el.style[prop] = converted;
2502
+ changed = true;
2503
+ }
2504
+ }
2505
+ for (const prop of COMPOUND_PROPS) {
2506
+ const value = computed[prop];
2507
+ if (!value || value === "none") continue;
2508
+ if (aggressive && prop !== "backgroundImage") {
2509
+ el.style[prop] = "none";
2510
+ changed = true;
2511
+ continue;
2512
+ }
2513
+ if (!hasUnsupportedColor(value)) continue;
2514
+ if (aggressive) {
2515
+ el.style[prop] = "none";
2516
+ changed = true;
2517
+ continue;
2518
+ }
2519
+ el.style[prop] = neutralizeColorFunctions(value);
2520
+ changed = true;
2521
+ }
2522
+ if (changed) touched++;
2523
+ }
2524
+ return touched;
2525
+ }
2526
+
2252
2527
  // src/lib/capture.ts
2253
2528
  var HTML2CANVAS_TIMEOUT_MS = 1e4;
2254
2529
  var FALLBACK_PAGE_BACKGROUND = "#ffffff";
@@ -2420,7 +2695,7 @@ function cropCanvas(full, rect, scale) {
2420
2695
  ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
2421
2696
  return out;
2422
2697
  }
2423
- async function captureViaDom(rect, sx, sy) {
2698
+ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2424
2699
  const { default: html2canvas } = await import('html2canvas');
2425
2700
  const scale = Math.min(window.devicePixelRatio || 1, 2);
2426
2701
  const vw = document.documentElement.clientWidth || window.innerWidth;
@@ -2438,14 +2713,19 @@ async function captureViaDom(rect, sx, sy) {
2438
2713
  useCORS: true,
2439
2714
  allowTaint: true,
2440
2715
  // The page's own background, never null — see resolvePageBackground().
2441
- backgroundColor: resolvePageBackground(),
2716
+ // Passed straight to html2canvas's parser, so it gets the same
2717
+ // CSS Color 4 treatment the document walk applies.
2718
+ backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
2442
2719
  logging: false,
2443
2720
  scrollX: sx,
2444
2721
  scrollY: sy,
2445
2722
  windowWidth: vw,
2446
2723
  windowHeight: vh,
2447
2724
  ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay"),
2448
- onclone: (doc) => pinStuckClones(doc)
2725
+ onclone: (doc) => {
2726
+ neutralizeDocumentColors(doc, aggressiveColors);
2727
+ pinStuckClones(doc);
2728
+ }
2449
2729
  }),
2450
2730
  HTML2CANVAS_TIMEOUT_MS
2451
2731
  );
@@ -2480,8 +2760,16 @@ async function captureRegion(rect, scroll, prefer = "auto") {
2480
2760
  const blob = await encodeShot(canvas);
2481
2761
  return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
2482
2762
  } catch (err) {
2483
- console.warn("[QA] region capture failed:", err);
2484
- return { status: "failed" };
2763
+ console.warn("[QA] region capture failed, retrying without gradients/shadows:", err);
2764
+ try {
2765
+ const canvas = await captureViaDom(rect, sx, sy, true);
2766
+ if (!canvas) return { status: "failed" };
2767
+ const blob = await encodeShot(canvas);
2768
+ return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
2769
+ } catch (retryErr) {
2770
+ console.warn("[QA] region capture failed after retry:", retryErr);
2771
+ return { status: "failed" };
2772
+ }
2485
2773
  }
2486
2774
  }
2487
2775
 
@@ -2904,6 +3192,7 @@ async function shareZipFile(blob, filename, title) {
2904
3192
  // src/lib/fsSync.ts
2905
3193
  var MAX_SLUG_LENGTH = 48;
2906
3194
  var FS_ROOT_META_KEY = "fsSyncRoot";
3195
+ var FS_CAMPAIGN_META_KEY = "fsSyncCampaign";
2907
3196
  var root = null;
2908
3197
  var campaignDir = null;
2909
3198
  var notesDir = null;
@@ -2912,6 +3201,7 @@ var campaign = null;
2912
3201
  var noteIndex = {};
2913
3202
  var state = "unsupported";
2914
3203
  var lastError = "";
3204
+ var metaStore = null;
2915
3205
  var bulkWriting = false;
2916
3206
  var listeners = /* @__PURE__ */ new Set();
2917
3207
  function setState(next, error = "") {
@@ -2934,8 +3224,20 @@ function onFsSyncChange(fn) {
2934
3224
  function isFsSyncSupported() {
2935
3225
  return typeof window !== "undefined" && typeof window.showDirectoryPicker === "function";
2936
3226
  }
3227
+ function isFsSyncAvailable() {
3228
+ return typeof window !== "undefined";
3229
+ }
3230
+ function getFsSyncEngine() {
3231
+ if (isFsSyncSupported() || typeof document === "undefined") return "disk";
3232
+ return "download";
3233
+ }
2937
3234
  function getFsSyncState() {
2938
- if (!isFsSyncSupported()) return "unsupported";
3235
+ if (!isFsSyncAvailable()) return "unsupported";
3236
+ if (!isFsSyncSupported()) {
3237
+ if (state === "syncing") return "syncing";
3238
+ if (state === "error") return "error";
3239
+ return "connected";
3240
+ }
2939
3241
  return state === "unsupported" ? "off" : state;
2940
3242
  }
2941
3243
  function getFsSyncError() {
@@ -2945,9 +3247,18 @@ function getFsSyncCampaign() {
2945
3247
  return campaign;
2946
3248
  }
2947
3249
  function getFsSyncPath() {
3250
+ if (getFsSyncEngine() === "download") return campaign ? campaignRelativePath() : "";
2948
3251
  if (!root) return "";
2949
3252
  if (!campaign) return root.name;
2950
- return `${root.name}/${safeSegment(campaign.project)}/${safeSegment(campaign.campaign)}`;
3253
+ return `${root.name}/${campaignRelativePath()}`;
3254
+ }
3255
+ function campaignRelativePath() {
3256
+ if (!campaign) return "";
3257
+ return `${safeSegment(campaign.project)}/${safeSegment(campaign.campaign)}`;
3258
+ }
3259
+ function campaignZipFileName() {
3260
+ if (!campaign) return "qa-campaign.zip";
3261
+ return `${safeSegment(campaign.project)} - ${safeSegment(campaign.campaign)}.zip`;
2951
3262
  }
2952
3263
  function safeSegment(input, fallback = "untitled") {
2953
3264
  let s = (input ?? "").normalize("NFC").replace(/[\\/:*?"<>|\u0000-\u001F]/g, "-");
@@ -2989,10 +3300,25 @@ async function permissionFor(handle, request) {
2989
3300
  return "granted";
2990
3301
  }
2991
3302
  async function restoreFsSync(store) {
2992
- if (!isFsSyncSupported()) {
3303
+ metaStore = store;
3304
+ if (!isFsSyncAvailable()) {
2993
3305
  setState("unsupported");
2994
3306
  return "unsupported";
2995
3307
  }
3308
+ if (getFsSyncEngine() === "download") {
3309
+ try {
3310
+ const saved = await store.getMeta(FS_CAMPAIGN_META_KEY);
3311
+ if (saved?.campaign?.project && saved.active !== false) {
3312
+ campaign = saved.campaign;
3313
+ noteIndex = saved.notes ?? {};
3314
+ setState("syncing");
3315
+ return "syncing";
3316
+ }
3317
+ } catch {
3318
+ }
3319
+ setState("connected");
3320
+ return "connected";
3321
+ }
2996
3322
  try {
2997
3323
  const saved = await store.getMeta(FS_ROOT_META_KEY);
2998
3324
  if (!saved || typeof saved.getDirectoryHandle !== "function") {
@@ -3009,7 +3335,11 @@ async function restoreFsSync(store) {
3009
3335
  }
3010
3336
  }
3011
3337
  async function chooseFsSyncRoot(store) {
3012
- if (!isFsSyncSupported()) return false;
3338
+ metaStore = store;
3339
+ if (!isFsSyncSupported()) {
3340
+ setState("connected");
3341
+ return true;
3342
+ }
3013
3343
  const picker = window.showDirectoryPicker;
3014
3344
  try {
3015
3345
  const handle = await picker({ id: "qapture-qa", mode: "readwrite", startIn: "documents" });
@@ -3057,6 +3387,30 @@ function describeError(err) {
3057
3387
  return e?.message || "write-failed";
3058
3388
  }
3059
3389
  async function openFsCampaign(input) {
3390
+ if (getFsSyncEngine() === "download") {
3391
+ const sameAsBefore = campaign?.project === input.project && campaign?.campaign === input.campaign;
3392
+ let startedAt = sameAsBefore && campaign ? campaign.startedAt : (/* @__PURE__ */ new Date()).toISOString();
3393
+ if (!sameAsBefore) {
3394
+ noteIndex = {};
3395
+ try {
3396
+ const saved = await metaStore?.getMeta(FS_CAMPAIGN_META_KEY);
3397
+ if (saved?.campaign?.project === input.project && saved.campaign.campaign === input.campaign) {
3398
+ noteIndex = saved.notes ?? {};
3399
+ startedAt = saved.campaign.startedAt || startedAt;
3400
+ }
3401
+ } catch {
3402
+ }
3403
+ }
3404
+ campaign = {
3405
+ project: input.project,
3406
+ campaign: input.campaign,
3407
+ tester: input.tester,
3408
+ startedAt
3409
+ };
3410
+ await persistDownloadCampaign();
3411
+ setState("syncing");
3412
+ return true;
3413
+ }
3060
3414
  if (!root) return false;
3061
3415
  try {
3062
3416
  const perm = await permissionFor(root, false);
@@ -3095,11 +3449,23 @@ async function openFsCampaign(input) {
3095
3449
  }
3096
3450
  }
3097
3451
  function closeFsCampaign() {
3452
+ if (getFsSyncEngine() === "download") void persistDownloadCampaign(false);
3098
3453
  campaignDir = notesDir = shotsDir = null;
3099
3454
  campaign = null;
3100
3455
  noteIndex = {};
3456
+ if (getFsSyncEngine() === "download") {
3457
+ setState("connected");
3458
+ return;
3459
+ }
3101
3460
  if (root) setState("connected");
3102
3461
  }
3462
+ async function persistDownloadCampaign(active = true) {
3463
+ if (!metaStore || !campaign) return;
3464
+ try {
3465
+ await metaStore.setMeta(FS_CAMPAIGN_META_KEY, { campaign, notes: noteIndex, active });
3466
+ } catch {
3467
+ }
3468
+ }
3103
3469
  async function writeFile(dir, name, data) {
3104
3470
  const handle = await dir.getFileHandle(name, { create: true });
3105
3471
  const writable = await handle.createWritable();
@@ -3130,6 +3496,13 @@ function entryFor(note) {
3130
3496
  };
3131
3497
  }
3132
3498
  async function syncNoteToDisk(note, allNotes) {
3499
+ if (getFsSyncEngine() === "download") {
3500
+ if (!campaign) return false;
3501
+ noteIndex[note.id] = entryFor(note);
3502
+ await persistDownloadCampaign();
3503
+ setState("syncing");
3504
+ return true;
3505
+ }
3133
3506
  if (!campaignDir || !notesDir || !shotsDir || !campaign) return false;
3134
3507
  try {
3135
3508
  const previous = noteIndex[note.id];
@@ -3157,6 +3530,12 @@ async function syncNoteToDisk(note, allNotes) {
3157
3530
  }
3158
3531
  }
3159
3532
  async function removeNoteFromDisk(noteId, allNotes) {
3533
+ if (getFsSyncEngine() === "download") {
3534
+ if (!noteIndex[noteId]) return true;
3535
+ delete noteIndex[noteId];
3536
+ await persistDownloadCampaign();
3537
+ return true;
3538
+ }
3160
3539
  if (!campaignDir || !notesDir || !shotsDir) return false;
3161
3540
  const entry = noteIndex[noteId];
3162
3541
  if (!entry) return true;
@@ -3182,8 +3561,15 @@ function noteMarkdownForDisk(note, entry) {
3182
3561
  }
3183
3562
  async function flushCampaignFiles(allNotes) {
3184
3563
  if (!campaignDir || !campaign) return;
3185
- const known = allNotes.filter((n) => noteIndex[n.id]);
3186
- const ordered = known.slice().sort((a, b) => (noteIndex[a.id].seq ?? 0) - (noteIndex[b.id].seq ?? 0));
3564
+ await writeFile(campaignDir, "campaign.json", campaignJsonText(allNotes));
3565
+ await writeFile(campaignDir, "REPORT.md", reportMarkdownText(allNotes));
3566
+ }
3567
+ function orderedKnownNotes(allNotes) {
3568
+ return allNotes.filter((n) => noteIndex[n.id]).slice().sort((a, b) => (noteIndex[a.id].seq ?? 0) - (noteIndex[b.id].seq ?? 0));
3569
+ }
3570
+ function campaignJsonText(allNotes) {
3571
+ if (!campaign) return "{}";
3572
+ const ordered = orderedKnownNotes(allNotes);
3187
3573
  const meta = {
3188
3574
  project: campaign.project,
3189
3575
  campaign: campaign.campaign,
@@ -3193,7 +3579,11 @@ async function flushCampaignFiles(allNotes) {
3193
3579
  noteCount: ordered.length,
3194
3580
  notes: noteIndex
3195
3581
  };
3196
- await writeFile(campaignDir, "campaign.json", JSON.stringify(meta, null, 2));
3582
+ return JSON.stringify(meta, null, 2);
3583
+ }
3584
+ function reportMarkdownText(allNotes) {
3585
+ if (!campaign) return "";
3586
+ const ordered = orderedKnownNotes(allNotes);
3197
3587
  const counts = { bug: 0, question: 0, polish: 0, verified: 0 };
3198
3588
  for (const n of ordered) {
3199
3589
  const sev = n.severity ?? "bug";
@@ -3207,20 +3597,66 @@ async function flushCampaignFiles(allNotes) {
3207
3597
  "",
3208
3598
  campaign.tester ? `Tester: ${campaign.tester} ` : "",
3209
3599
  `Started: ${campaign.startedAt} `,
3210
- `Updated: ${meta.updatedAt} `,
3600
+ `Updated: ${(/* @__PURE__ */ new Date()).toISOString()} `,
3211
3601
  `Points: ${ordered.length} (${counts.bug} bug, ${counts.question} question, ${counts.polish} polish \xB7 ${counts.verified} verified)`,
3212
3602
  "",
3213
- "Written live by Qapture as each point was saved. Screenshots are in",
3603
+ 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",
3214
3604
  "`screenshots/`, one Markdown file per point in `notes/`.",
3215
3605
  "",
3216
3606
  "---",
3217
3607
  ""
3218
3608
  ].filter((line) => line !== "").join("\n");
3219
3609
  const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
3220
- await writeFile(campaignDir, "REPORT.md", `${header}
3221
- ${body}`);
3610
+ return `${header}
3611
+ ${body}`;
3612
+ }
3613
+ async function buildCampaignZipBlob(allNotes) {
3614
+ if (!campaign || typeof document === "undefined") return null;
3615
+ try {
3616
+ const { default: JSZip } = await import('jszip');
3617
+ const zip = new JSZip();
3618
+ const base = zip.folder(safeSegment(campaign.project, "Project"))?.folder(safeSegment(campaign.campaign, defaultCampaignName()));
3619
+ if (!base) return null;
3620
+ base.file("campaign.json", campaignJsonText(allNotes));
3621
+ base.file("REPORT.md", reportMarkdownText(allNotes));
3622
+ const notesFolder = base.folder("notes");
3623
+ const shotsFolder = base.folder("screenshots");
3624
+ for (const note of orderedKnownNotes(allNotes)) {
3625
+ const entry = noteIndex[note.id];
3626
+ notesFolder?.file(entry.file, noteMarkdownForDisk(note, entry));
3627
+ if (note.screenshot && entry.shot) shotsFolder?.file(entry.shot, note.screenshot);
3628
+ if (note.afterScreenshot && entry.after) shotsFolder?.file(entry.after, note.afterScreenshot);
3629
+ }
3630
+ return await zip.generateAsync({ type: "blob" });
3631
+ } catch (err) {
3632
+ setState("error", describeError(err));
3633
+ return null;
3634
+ }
3635
+ }
3636
+ async function downloadCampaignZip(allNotes) {
3637
+ const blob = await buildCampaignZipBlob(allNotes);
3638
+ if (!blob) return false;
3639
+ const url = URL.createObjectURL(blob);
3640
+ const a = document.createElement("a");
3641
+ a.href = url;
3642
+ a.download = campaignZipFileName();
3643
+ document.body.appendChild(a);
3644
+ a.click();
3645
+ a.remove();
3646
+ setTimeout(() => URL.revokeObjectURL(url), 2e3);
3647
+ setState("syncing");
3648
+ return true;
3222
3649
  }
3223
3650
  async function syncAllToDisk(allNotes) {
3651
+ if (getFsSyncEngine() === "download") {
3652
+ if (!campaign) return false;
3653
+ for (const note of allNotes.slice().reverse()) {
3654
+ if (!noteIndex[note.id]) noteIndex[note.id] = entryFor(note);
3655
+ }
3656
+ await persistDownloadCampaign();
3657
+ setState("syncing");
3658
+ return true;
3659
+ }
3224
3660
  if (!campaignDir) return false;
3225
3661
  const ordered = allNotes.slice().reverse();
3226
3662
  bulkWriting = true;
@@ -4016,6 +4452,15 @@ function QaProvider({
4016
4452
  closeFsCampaign();
4017
4453
  notify(t("sync_paused"), { id: "sync-state" });
4018
4454
  }, [notify, t]);
4455
+ const saveSyncFolderNow = useCallback(async () => {
4456
+ const name = campaignZipFileName();
4457
+ const ok = await downloadCampaignZip(notesRef.current);
4458
+ notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
4459
+ tone: ok ? "success" : "error",
4460
+ id: "sync-zip"
4461
+ });
4462
+ return ok;
4463
+ }, [notify, t]);
4019
4464
  const forgetSyncFolder = useCallback(async () => {
4020
4465
  await disconnectFsSync(idb);
4021
4466
  storage.setJSON(LAST_CAMPAIGN_KEY, {});
@@ -4064,10 +4509,23 @@ function QaProvider({
4064
4509
  useEffect(() => {
4065
4510
  if (!autoBackup) return;
4066
4511
  if (notes.length === 0 || isExporting) return;
4067
- if (getFsSyncState() === "syncing") return;
4512
+ const syncing = getFsSyncState() === "syncing";
4513
+ const viaDownload = getFsSyncEngine() === "download";
4514
+ if (syncing && !viaDownload) return;
4068
4515
  const lastAt = Number(storage.getItem(AUTO_BACKUP_AT_KEY) ?? "0") || 0;
4069
4516
  if (notes.length < lastAt + AUTO_BACKUP_EVERY) return;
4070
4517
  storage.setItem(AUTO_BACKUP_AT_KEY, String(notes.length));
4518
+ if (syncing && viaDownload) {
4519
+ const name = campaignZipFileName();
4520
+ void downloadCampaignZip(notesRef.current).then((ok) => {
4521
+ notify(ok ? t("sync_zip_saved", { name }) : t("sync_zip_failed"), {
4522
+ tone: ok ? "success" : "error",
4523
+ id: "sync-zip",
4524
+ duration: 3e3
4525
+ });
4526
+ });
4527
+ return;
4528
+ }
4071
4529
  const stamp = nowIso().slice(0, 16).replace(/[:T]/g, "-");
4072
4530
  void exportZipRef.current(`qa-autosave-${stamp}-${notes.length}`).then(() => {
4073
4531
  notify(t("autosave_done", { n: notes.length }), { id: "autosave", duration: 3e3 });
@@ -4368,6 +4826,7 @@ function QaProvider({
4368
4826
  }, []);
4369
4827
  useEffect(() => {
4370
4828
  if (typeof window === "undefined") return;
4829
+ if (notesLoading) return;
4371
4830
  const params = new URLSearchParams(window.location.search);
4372
4831
  const raw = params.get("qa");
4373
4832
  if (!raw || !raw.startsWith("walk")) return;
@@ -4386,7 +4845,7 @@ function QaProvider({
4386
4845
  } else {
4387
4846
  startWalk("plan", 0);
4388
4847
  }
4389
- }, [startWalk]);
4848
+ }, [startWalk, notesLoading]);
4390
4849
  const value = {
4391
4850
  // Data
4392
4851
  notes,
@@ -4407,7 +4866,10 @@ function QaProvider({
4407
4866
  brand: config.brand,
4408
4867
  loginField: config.loginField,
4409
4868
  credentials: config.credentials,
4410
- journey: config.journey,
4869
+ // Never the raw config value: an unconfigured project falls back to the
4870
+ // generic plan so the Guide is never a blank page.
4871
+ journey: journeyOrEmpty(config.journey),
4872
+ journeyIsGeneric: !config.journey?.some((lane) => lane.steps.length > 0),
4411
4873
  preamble: config.preamble,
4412
4874
  // i18n helpers
4413
4875
  t,
@@ -4455,7 +4917,8 @@ function QaProvider({
4455
4917
  // rebuilt when fsSync's module state changes (path/campaign live outside
4456
4918
  // React).
4457
4919
  sync: {
4458
- supported: isFsSyncSupported(),
4920
+ supported: isFsSyncAvailable(),
4921
+ engine: getFsSyncEngine(),
4459
4922
  state: syncState,
4460
4923
  path: syncTick >= 0 ? getFsSyncPath() : "",
4461
4924
  campaign: getFsSyncCampaign(),
@@ -4465,6 +4928,7 @@ function QaProvider({
4465
4928
  reconnectSyncFolder,
4466
4929
  startSyncCampaign,
4467
4930
  stopSyncCampaign,
4931
+ saveSyncFolderNow,
4468
4932
  forgetSyncFolder,
4469
4933
  suggestCampaignName,
4470
4934
  lastCampaign,
@@ -4509,7 +4973,9 @@ function QaProvider({
4509
4973
  return /* @__PURE__ */ jsx(QaContext.Provider, { value, children });
4510
4974
  }
4511
4975
  function journeyOrEmpty(journey) {
4512
- return Array.isArray(journey) ? journey : [];
4976
+ const configured = Array.isArray(journey) ? journey : [];
4977
+ const hasSteps = configured.some((lane) => lane.steps.length > 0);
4978
+ return hasSteps ? configured : FALLBACK_JOURNEY;
4513
4979
  }
4514
4980
  function useQa() {
4515
4981
  const ctx = useContext(QaContext);
@@ -6490,6 +6956,7 @@ function SettingsSheet({ onClose }) {
6490
6956
  forgetSyncFolder,
6491
6957
  suggestCampaignName,
6492
6958
  lastCampaign,
6959
+ saveSyncFolderNow,
6493
6960
  storageHealth,
6494
6961
  refreshStorageHealth,
6495
6962
  requestPersistentStorage: requestPersistentStorage2,
@@ -6531,6 +6998,7 @@ function SettingsSheet({ onClose }) {
6531
6998
  }
6532
6999
  };
6533
7000
  const syncing = sync.state === "syncing";
7001
+ const viaZip = sync.engine === "download";
6534
7002
  const quotaKnown = storageHealth.supported && storageHealth.quotaBytes > 0;
6535
7003
  const usedPct = quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
6536
7004
  const meterColor = storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
@@ -6558,6 +7026,7 @@ function SettingsSheet({ onClose }) {
6558
7026
  /* @__PURE__ */ jsxs("div", { className: "qa-flex-1 qa-space-y-4 qa-overflow-y-auto qa-p-3", children: [
6559
7027
  /* @__PURE__ */ jsxs(Section, { icon: "Folder", title: t("sync_title"), children: [
6560
7028
  /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("sync_hint") }),
7029
+ viaZip ? /* @__PURE__ */ 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,
6561
7030
  !sync.supported ? /* @__PURE__ */ 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__ */ jsxs(
6562
7031
  "button",
6563
7032
  {
@@ -6585,24 +7054,41 @@ function SettingsSheet({ onClose }) {
6585
7054
  ]
6586
7055
  }
6587
7056
  ) : syncing ? /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", children: [
7057
+ viaZip && /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo", children: t("sync_zip_where") }),
6588
7058
  /* @__PURE__ */ 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: [
6589
7059
  /* @__PURE__ */ jsx(Icon, { name: "FolderCheck", size: 13, className: "qa-shrink-0" }),
6590
7060
  /* @__PURE__ */ jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", title: sync.path, children: sync.path })
6591
7061
  ] }),
6592
- /* @__PURE__ */ jsx(
6593
- "button",
6594
- {
6595
- type: "button",
6596
- onClick: stopSyncCampaign,
6597
- className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
6598
- style: { background: "transparent", cursor: "pointer" },
6599
- children: t("sync_stop")
6600
- }
6601
- )
7062
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-flex-wrap qa-gap-2", children: [
7063
+ viaZip && /* @__PURE__ */ jsxs(
7064
+ "button",
7065
+ {
7066
+ type: "button",
7067
+ disabled: busy,
7068
+ onClick: () => void run2(saveSyncFolderNow),
7069
+ 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",
7070
+ style: { border: "none", cursor: "pointer" },
7071
+ children: [
7072
+ /* @__PURE__ */ jsx(Icon, { name: "Download", size: 13 }),
7073
+ t("sync_zip_save_now")
7074
+ ]
7075
+ }
7076
+ ),
7077
+ /* @__PURE__ */ jsx(
7078
+ "button",
7079
+ {
7080
+ type: "button",
7081
+ onClick: stopSyncCampaign,
7082
+ className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
7083
+ style: { background: "transparent", cursor: "pointer" },
7084
+ children: t("sync_stop")
7085
+ }
7086
+ )
7087
+ ] })
6602
7088
  ] }) : (
6603
7089
  /* connected: folder chosen, campaign not open yet */
6604
7090
  /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", children: [
6605
- /* @__PURE__ */ jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
7091
+ !viaZip && /* @__PURE__ */ jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-10 qa-text-mid", children: [
6606
7092
  /* @__PURE__ */ jsx(Icon, { name: "Folder", size: 12, className: "qa-shrink-0" }),
6607
7093
  /* @__PURE__ */ jsx("span", { className: "qa-truncate qa-dir-ltr", dir: "ltr", children: sync.path })
6608
7094
  ] }),
@@ -6624,11 +7110,11 @@ function SettingsSheet({ onClose }) {
6624
7110
  style: { border: "none", cursor: "pointer" },
6625
7111
  children: [
6626
7112
  /* @__PURE__ */ jsx(Icon, { name: "Check", size: 13 }),
6627
- t("sync_start")
7113
+ viaZip ? t("sync_zip_start") : t("sync_start")
6628
7114
  ]
6629
7115
  }
6630
7116
  ),
6631
- /* @__PURE__ */ jsx(
7117
+ !viaZip && /* @__PURE__ */ jsx(
6632
7118
  "button",
6633
7119
  {
6634
7120
  type: "button",
@@ -7138,12 +7624,31 @@ function Lane({
7138
7624
  ] });
7139
7625
  }
7140
7626
  function GuideSection() {
7141
- const { guideChecked, toggleGuide, t, journey, pick: pick2, lang, startTestAlong, startWalk, testAlongSteps } = useQa();
7627
+ const {
7628
+ guideChecked,
7629
+ toggleGuide,
7630
+ t,
7631
+ journey,
7632
+ journeyIsGeneric,
7633
+ pick: pick2,
7634
+ lang,
7635
+ startTestAlong,
7636
+ startWalk,
7637
+ testAlongSteps
7638
+ } = useQa();
7142
7639
  const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
7143
7640
  const done = all.filter((k) => guideChecked.has(k)).length;
7144
7641
  const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
7145
7642
  const coverage = computeCoverage(journey, guideChecked);
7146
7643
  return /* @__PURE__ */ jsxs("div", { className: "qa-space-y-3", children: [
7644
+ journeyIsGeneric && /* @__PURE__ */ jsx(
7645
+ "p",
7646
+ {
7647
+ 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",
7648
+ "data-qa-generic-plan": "true",
7649
+ children: t("journey_generic")
7650
+ }
7651
+ ),
7147
7652
  /* @__PURE__ */ jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
7148
7653
  coverage.red.total > 0 && /* @__PURE__ */ jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-1.5 qa-text-11", children: [
7149
7654
  /* @__PURE__ */ 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" }),
@@ -7200,8 +7705,7 @@ function GuideSection() {
7200
7705
  pick: pick2
7201
7706
  },
7202
7707
  g.id
7203
- )),
7204
- journey.length === 0 && /* @__PURE__ */ jsx("p", { className: "qa-py-8 qa-text-center qa-text-sm qa-text-slate-400", children: t("tab_guide") })
7708
+ ))
7205
7709
  ] });
7206
7710
  }
7207
7711
  var TABS = [
@@ -8046,7 +8550,7 @@ function WalkHud() {
8046
8550
  )
8047
8551
  ] });
8048
8552
  }
8049
- const onThisPage = typeof window !== "undefined" && stop.path === window.location.pathname;
8553
+ const onThisPage = stop.path === ANY_PAGE || typeof window !== "undefined" && stop.path === window.location.pathname;
8050
8554
  return /* @__PURE__ */ jsxs(
8051
8555
  "div",
8052
8556
  {
@@ -9144,6 +9648,7 @@ function CaptureMode() {
9144
9648
  type: "button",
9145
9649
  onClick: () => selection && void runCapture(selection.rect),
9146
9650
  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",
9651
+ style: { background: "transparent", cursor: "pointer" },
9147
9652
  children: [
9148
9653
  /* @__PURE__ */ jsx(Icon, { name: "RotateCcw", size: 13 }),
9149
9654
  t("retry")
@@ -9443,5 +9948,5 @@ function Qapture({ config }) {
9443
9948
  }
9444
9949
 
9445
9950
  export { Qapture, deleteQaDatabase, initQaStudio };
9446
- //# sourceMappingURL=chunk-QBBWY3BF.js.map
9447
- //# sourceMappingURL=chunk-QBBWY3BF.js.map
9951
+ //# sourceMappingURL=chunk-JSEFC2VP.js.map
9952
+ //# sourceMappingURL=chunk-JSEFC2VP.js.map