qapture2 0.7.7 → 0.7.8

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.
@@ -1869,6 +1869,13 @@ var STR = {
1869
1869
  retest_not_found: "Can't find that spot on this page \u2014 open the right page first",
1870
1870
  before_label: "Before",
1871
1871
  after_label: "After",
1872
+ followup_label: "What happened this time",
1873
+ followup_add: "Say what happened",
1874
+ followup_placeholder: "What did it do this time, and how is that different from what you asked for?",
1875
+ followup_hint: "The first note stays as it is. This is added under it as round two.",
1876
+ keep_only_retest: "Keep only these {n}",
1877
+ keep_only_retest_q: "Delete the other {n}?",
1878
+ keep_only_retest_hint: "Delete every note that is not waiting to be re-tested",
1872
1879
  welcome_title: "Testing {brand}",
1873
1880
  welcome_capture: 'Click "Capture from page", then click whatever looks wrong.',
1874
1881
  welcome_say: "Say what you expected in your own words. A screenshot is taken for you.",
@@ -2067,6 +2074,13 @@ var STR = {
2067
2074
  retest_not_found: "\u062A\u0639\u0630\u0651\u0631 \u0625\u064A\u062C\u0627\u062F \u0630\u0644\u0643 \u0627\u0644\u0645\u0648\u0636\u0639 \u0641\u064A \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629 \u2014 \u0627\u0641\u062A\u062D \u0627\u0644\u0635\u0641\u062D\u0629 \u0627\u0644\u0635\u062D\u064A\u062D\u0629 \u0623\u0648\u0644\u064B\u0627",
2068
2075
  before_label: "\u0642\u0628\u0644",
2069
2076
  after_label: "\u0628\u0639\u062F",
2077
+ followup_label: "\u0645\u0627\u0630\u0627 \u062D\u062F\u062B \u0647\u0630\u0647 \u0627\u0644\u0645\u0631\u0629",
2078
+ followup_add: "\u0627\u0643\u062A\u0628 \u0645\u0627\u0630\u0627 \u062D\u062F\u062B",
2079
+ followup_placeholder: "\u0645\u0627\u0630\u0627 \u0641\u0639\u0644 \u0647\u0630\u0647 \u0627\u0644\u0645\u0631\u0629\u060C \u0648\u0643\u064A\u0641 \u064A\u062E\u062A\u0644\u0641 \u0630\u0644\u0643 \u0639\u0645\u0651\u0627 \u0637\u0644\u0628\u062A\u0647\u061F",
2080
+ followup_hint: "\u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0629 \u0627\u0644\u0623\u0648\u0644\u0649 \u062A\u0628\u0642\u0649 \u0643\u0645\u0627 \u0647\u064A. \u0647\u0630\u0647 \u062A\u064F\u0636\u0627\u0641 \u062A\u062D\u062A\u0647\u0627 \u0643\u062C\u0648\u0644\u0629 \u062B\u0627\u0646\u064A\u0629.",
2081
+ keep_only_retest: "\u0623\u0628\u0642\u0650 \u0647\u0630\u0647 \u0627\u0644\u0640 {n} \u0641\u0642\u0637",
2082
+ keep_only_retest_q: "\u062D\u0630\u0641 \u0627\u0644\u0640 {n} \u0627\u0644\u0623\u062E\u0631\u0649\u061F",
2083
+ keep_only_retest_hint: "\u062D\u0630\u0641 \u0643\u0644 \u0645\u0644\u0627\u062D\u0638\u0629 \u0644\u064A\u0633\u062A \u0628\u0627\u0646\u062A\u0638\u0627\u0631 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
2070
2084
  welcome_title: "\u0627\u062E\u062A\u0628\u0627\u0631 {brand}",
2071
2085
  welcome_capture: "\u0627\u0636\u063A\u0637 \xAB\u0627\u0644\u062A\u0642\u0627\u0637 \u0645\u0646 \u0627\u0644\u0635\u0641\u062D\u0629\xBB \u062B\u0645 \u0627\u0636\u063A\u0637 \u0639\u0644\u0649 \u0645\u0627 \u064A\u0628\u062F\u0648 \u062E\u0627\u0637\u0626\u064B\u0627.",
2072
2086
  welcome_say: "\u0627\u0643\u062A\u0628 \u0645\u0627 \u0643\u0646\u062A \u062A\u062A\u0648\u0642\u0639\u0647 \u0628\u0643\u0644\u0645\u0627\u062A\u0643. \u062A\u064F\u0644\u062A\u0642\u0637 \u0627\u0644\u0635\u0648\u0631\u0629 \u062A\u0644\u0642\u0627\u0626\u064A\u064B\u0627.",
@@ -2111,18 +2125,6 @@ function pick(value, lang) {
2111
2125
  if (typeof value === "string") return value;
2112
2126
  return value[lang] ?? value.en ?? "";
2113
2127
  }
2114
-
2115
- // src/lib/coverage.ts
2116
- var RISK_COLORS = {
2117
- red: "#FF6B6B",
2118
- // --qa-danger
2119
- amber: "#FBBF24",
2120
- // --qa-warn
2121
- green: "#34D399",
2122
- // --qa-success
2123
- none: "#5B616B"
2124
- // --qa-neutral
2125
- };
2126
2128
  function computeCoverage(journey, guideChecked, skipped) {
2127
2129
  const red = { total: 0, covered: 0 };
2128
2130
  const amber = { total: 0, covered: 0 };
@@ -2976,16 +2978,19 @@ function clipToPaintedArea(el, rect) {
2976
2978
  let top = rect.top;
2977
2979
  let right = rect.left + rect.width;
2978
2980
  let bottom = rect.top + rect.height;
2981
+ const doc = el.ownerDocument;
2982
+ const root2 = doc ? doc.documentElement : null;
2983
+ const body = doc ? doc.body : null;
2984
+ const rootStyle = root2 ? safeComputedStyle(root2) : null;
2985
+ const bodyStyle = body ? safeComputedStyle(body) : null;
2979
2986
  let node = el.parentElement;
2980
- while (node) {
2981
- let style;
2982
- try {
2983
- style = getComputedStyle(node);
2984
- } catch {
2985
- break;
2986
- }
2987
- const clipsX = style.overflowX !== "visible";
2988
- const clipsY = style.overflowY !== "visible";
2987
+ while (node && node !== root2) {
2988
+ const style = safeComputedStyle(node);
2989
+ if (!style) break;
2990
+ const propagatedX = node === body && rootStyle?.overflowX === "visible";
2991
+ const propagatedY = node === body && rootStyle?.overflowY === "visible";
2992
+ const clipsX = !propagatedX && style.overflowX !== "visible";
2993
+ const clipsY = !propagatedY && style.overflowY !== "visible";
2989
2994
  if (clipsX || clipsY) {
2990
2995
  const b = node.getBoundingClientRect();
2991
2996
  const padLeft = b.left + node.clientLeft;
@@ -3001,9 +3006,33 @@ function clipToPaintedArea(el, rect) {
3001
3006
  }
3002
3007
  node = node.parentElement;
3003
3008
  }
3009
+ const vpX = usedViewportOverflow(rootStyle?.overflowX, bodyStyle?.overflowX);
3010
+ const vpY = usedViewportOverflow(rootStyle?.overflowY, bodyStyle?.overflowY);
3011
+ if (vpX === "hidden" || vpX === "clip" || vpY === "hidden" || vpY === "clip") {
3012
+ const { vw, vh } = viewportSize();
3013
+ if (vpX === "hidden" || vpX === "clip") {
3014
+ left = Math.max(left, 0);
3015
+ right = Math.min(right, vw);
3016
+ }
3017
+ if (vpY === "hidden" || vpY === "clip") {
3018
+ top = Math.max(top, 0);
3019
+ bottom = Math.min(bottom, vh);
3020
+ }
3021
+ }
3004
3022
  if (right - left < 2 || bottom - top < 2) return rect;
3005
3023
  return { left, top, width: right - left, height: bottom - top };
3006
3024
  }
3025
+ function safeComputedStyle(node) {
3026
+ try {
3027
+ return getComputedStyle(node);
3028
+ } catch {
3029
+ return null;
3030
+ }
3031
+ }
3032
+ function usedViewportOverflow(rootValue, bodyValue) {
3033
+ if (rootValue && rootValue !== "visible") return rootValue;
3034
+ return bodyValue || "visible";
3035
+ }
3007
3036
  function intersectViewport(rect) {
3008
3037
  const { vw, vh } = viewportSize();
3009
3038
  const left = Math.max(0, rect.left);
@@ -3198,6 +3227,16 @@ function noteToMarkdown(note, opts) {
3198
3227
  }
3199
3228
  lines.push("");
3200
3229
  lines.push(oneLine(note.description) ? note.description.trim() : "_(no description)_");
3230
+ if (note.followUp && oneLine(note.followUp)) {
3231
+ lines.push("");
3232
+ lines.push(
3233
+ "> **This one came back \u2014 round 2.** The point above was already worked on once and it is still not right. What follows is the tester re-testing it, so treat it as the current ask and the point above as the history of what was originally wanted."
3234
+ );
3235
+ lines.push("");
3236
+ lines.push(`**What happened this time**${note.followUpAt ? ` (${oneLine(note.followUpAt)})` : ""}`);
3237
+ lines.push("");
3238
+ lines.push(note.followUp.trim());
3239
+ }
3201
3240
  const recordedSteps = note.context?.steps ?? [];
3202
3241
  if (recordedSteps.length) {
3203
3242
  const t0 = Date.parse(note.timestamp) || recordedSteps[recordedSteps.length - 1].t;
@@ -3284,7 +3323,7 @@ function fmtPct(n, d) {
3284
3323
  }
3285
3324
  function summariseSession(notes, stamp) {
3286
3325
  if (!notes.length) return "No points captured.";
3287
- let bugs = 0, enhancements = 0, designs = 0, verified = 0, awaiting = 0;
3326
+ let bugs = 0, enhancements = 0, designs = 0, verified = 0, awaiting = 0, secondRound = 0;
3288
3327
  const pages = /* @__PURE__ */ new Set();
3289
3328
  let earliest = Number.POSITIVE_INFINITY;
3290
3329
  let latest = 0;
@@ -3296,6 +3335,7 @@ function summariseSession(notes, stamp) {
3296
3335
  const status = n.status ?? "open";
3297
3336
  if (status === "verified") verified++;
3298
3337
  else if (status === "fixed") awaiting++;
3338
+ if (n.followUp && n.followUp.trim()) secondRound++;
3299
3339
  pages.add((n.route || "/").split("?")[0]);
3300
3340
  const t = Date.parse(n.timestamp);
3301
3341
  if (Number.isFinite(t)) {
@@ -3311,6 +3351,7 @@ function summariseSession(notes, stamp) {
3311
3351
  if (designs) parts.push(`${designs} design`);
3312
3352
  if (verified) parts.push(`${verified} verified`);
3313
3353
  if (awaiting) parts.push(`${awaiting} awaiting re-test`);
3354
+ if (secondRound) parts.push(`${secondRound} back for a second round`);
3314
3355
  parts.push(`${pages.size} page${pages.size === 1 ? "" : "s"}`);
3315
3356
  if (Number.isFinite(earliest) && latest > earliest) {
3316
3357
  const minutes = Math.max(1, Math.round((latest - earliest) / 6e4));
@@ -3365,6 +3406,14 @@ The tester wants something added or changed: a new section, a new feature, a dif
3365
3406
 
3366
3407
  Push back only when you have a real reason: it would break something, or it conflicts with an invariant below. Then say so plainly and propose the alternative you would build instead.
3367
3408
 
3409
+ ### A point with a "round 2" on it
3410
+
3411
+ Some points carry a **round 2** block: the same finding, already worked on once, re-tested, and still not right. Two things follow from that.
3412
+
3413
+ First, **the round-2 text is the current ask** and the paragraph above it is history \u2014 read the history to understand what was wanted, act on the round 2.
3414
+
3415
+ Second, **the previous attempt is evidence.** Something about the first reading was wrong, so do not simply do it again more carefully. Work out what was misunderstood the first time and say so in one sentence, then fix that. If a "Before"/"After" screenshot pair is attached, the After is what the tester was looking at when they wrote the round 2.
3416
+
3368
3417
  ### When the tag and the words disagree
3369
3418
 
3370
3419
  The tag sets your starting posture; **the words win.** A point tagged Design that says "this crashes" is a bug \u2014 treat it as one, and mention that you changed lane. People pick the wrong chip all the time; that is not a reason to do the wrong work.
@@ -4609,6 +4658,11 @@ function QaProvider({
4609
4658
  }
4610
4659
  if (patch.severity !== void 0) updated.severity = patch.severity;
4611
4660
  if (patch.status !== void 0) updated.status = patch.status;
4661
+ if (patch.followUp !== void 0) {
4662
+ const text = patch.followUp.trim();
4663
+ updated.followUp = text || void 0;
4664
+ updated.followUpAt = text ? nowIso() : void 0;
4665
+ }
4612
4666
  applyNotes((prev) => prev.map((n) => n.id === id ? updated : n));
4613
4667
  const persisted = await idb.put(updated);
4614
4668
  if (!persisted) {
@@ -6604,6 +6658,8 @@ function NoteItem({
6604
6658
  const thumbUrl = useObjectUrl(editing ? img ?? void 0 : note.screenshot);
6605
6659
  const afterUrl = useObjectUrl(note.afterScreenshot);
6606
6660
  const [retesting, setRetesting] = React.useState(false);
6661
+ const [editingFollowUp, setEditingFollowUp] = React.useState(false);
6662
+ const [followUpDraft, setFollowUpDraft] = React.useState(note.followUp ?? "");
6607
6663
  const [open, setOpen] = React.useState(false);
6608
6664
  const severity = note.severity ?? "bug";
6609
6665
  const status = note.status ?? "open";
@@ -6937,7 +6993,96 @@ function NoteItem({
6937
6993
  style: { borderColor: "var(--qa-success)" }
6938
6994
  }
6939
6995
  )
6940
- ] })
6996
+ ] }),
6997
+ (status === "fixed" || note.followUp) && /* @__PURE__ */ jsxRuntime.jsx(
6998
+ "div",
6999
+ {
7000
+ className: "qa-mt-2 qa-rounded-lg qa-border qa-border-subtle qa-bg-2 qa-p-2",
7001
+ "data-qa-followup": "true",
7002
+ children: editingFollowUp ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7003
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "qa-text-10 qa-text-lo", htmlFor: `qa-fu-${note.id}`, children: t("followup_label") }),
7004
+ /* @__PURE__ */ jsxRuntime.jsx(
7005
+ "textarea",
7006
+ {
7007
+ id: `qa-fu-${note.id}`,
7008
+ value: followUpDraft,
7009
+ onChange: (e) => setFollowUpDraft(e.target.value),
7010
+ placeholder: t("followup_placeholder"),
7011
+ rows: 3,
7012
+ className: "qa-mt-1 qa-w-full qa-rounded-lg qa-border qa-border-subtle qa-bg-1 qa-p-2 qa-text-xs qa-text-hi",
7013
+ style: { outline: "none", resize: "vertical" }
7014
+ }
7015
+ ),
7016
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-mt-1 qa-text-10 qa-text-lo", children: t("followup_hint") }),
7017
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-1.5 qa-flex qa-gap-2", children: [
7018
+ /* @__PURE__ */ jsxRuntime.jsxs(
7019
+ "button",
7020
+ {
7021
+ type: "button",
7022
+ onClick: () => {
7023
+ void updateNote(note.id, { followUp: followUpDraft });
7024
+ setEditingFollowUp(false);
7025
+ },
7026
+ className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-rounded-lg qa-bg-accent qa-px-2 qa-py-1 qa-text-11 qa-font-semibold",
7027
+ style: { border: "none", cursor: "pointer" },
7028
+ children: [
7029
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 12 }),
7030
+ t("save")
7031
+ ]
7032
+ }
7033
+ ),
7034
+ /* @__PURE__ */ jsxRuntime.jsx(
7035
+ "button",
7036
+ {
7037
+ type: "button",
7038
+ onClick: () => {
7039
+ setFollowUpDraft(note.followUp ?? "");
7040
+ setEditingFollowUp(false);
7041
+ },
7042
+ className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-11 qa-text-mid",
7043
+ style: { background: "transparent", cursor: "pointer" },
7044
+ children: t("cancel")
7045
+ }
7046
+ )
7047
+ ] })
7048
+ ] }) : note.followUp ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7049
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
7050
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 11, className: "qa-shrink-0 qa-text-warn" }),
7051
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-10 qa-text-warn", children: t("followup_label") }),
7052
+ /* @__PURE__ */ jsxRuntime.jsx(
7053
+ "button",
7054
+ {
7055
+ type: "button",
7056
+ onClick: () => {
7057
+ setFollowUpDraft(note.followUp ?? "");
7058
+ setEditingFollowUp(true);
7059
+ },
7060
+ "aria-label": t("edit"),
7061
+ className: "qa-tap-icon qa-ms-auto qa-text-lo",
7062
+ style: { background: "transparent", border: "none", cursor: "pointer" },
7063
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Pencil", size: 11 })
7064
+ }
7065
+ )
7066
+ ] }),
7067
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-mt-1 qa-whitespace-pre-wrap qa-break-words qa-text-xs qa-text-hi", children: note.followUp })
7068
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
7069
+ "button",
7070
+ {
7071
+ type: "button",
7072
+ onClick: () => {
7073
+ setFollowUpDraft("");
7074
+ setEditingFollowUp(true);
7075
+ },
7076
+ className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-text-11 qa-font-medium qa-text-accent",
7077
+ style: { background: "transparent", border: "none", cursor: "pointer" },
7078
+ children: [
7079
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Plus", size: 12 }),
7080
+ t("followup_add")
7081
+ ]
7082
+ }
7083
+ )
7084
+ }
7085
+ )
6941
7086
  ] })
6942
7087
  ]
6943
7088
  }
@@ -7170,8 +7315,11 @@ function FilterChip({ chip }) {
7170
7315
  );
7171
7316
  }
7172
7317
  function NoteFilterBar() {
7173
- const { filter, setFilter, noteCounts, notes, t } = useQa();
7318
+ const { filter, setFilter, noteCounts, notes, deleteNotes, t } = useQa();
7319
+ const [confirmPrune, setConfirmPrune] = React.useState(false);
7174
7320
  if (notes.length < 3) return null;
7321
+ const others = notes.filter((n) => (n.status ?? "open") !== "fixed");
7322
+ const showPrune = filter.status === "fixed" && noteCounts.fixed > 0 && others.length > 0;
7175
7323
  const sev = (key) => () => setFilter({ severity: filter.severity === key ? "all" : key });
7176
7324
  const stat = (key) => () => setFilter({ status: filter.status === key ? "all" : key });
7177
7325
  const chips = [
@@ -7276,7 +7424,48 @@ function NoteFilterBar() {
7276
7424
  ]
7277
7425
  }
7278
7426
  ),
7279
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsx(FilterChip, { chip }, chip.key)) })
7427
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsx(FilterChip, { chip }, chip.key)) }),
7428
+ showPrune && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-text-11", "data-qa-prune": "true", children: confirmPrune ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-mid", children: [
7429
+ t("keep_only_retest_q", { n: others.length }),
7430
+ " ",
7431
+ /* @__PURE__ */ jsxRuntime.jsx(
7432
+ "button",
7433
+ {
7434
+ type: "button",
7435
+ onClick: () => {
7436
+ void deleteNotes(others.map((n) => n.id));
7437
+ setConfirmPrune(false);
7438
+ },
7439
+ className: "qa-font-semibold qa-text-danger qa-tap",
7440
+ style: { background: "transparent", border: "none", cursor: "pointer" },
7441
+ children: t("yes")
7442
+ }
7443
+ ),
7444
+ " / ",
7445
+ /* @__PURE__ */ jsxRuntime.jsx(
7446
+ "button",
7447
+ {
7448
+ type: "button",
7449
+ onClick: () => setConfirmPrune(false),
7450
+ className: "qa-text-accent qa-tap",
7451
+ style: { background: "transparent", border: "none", cursor: "pointer" },
7452
+ children: t("no")
7453
+ }
7454
+ )
7455
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
7456
+ "button",
7457
+ {
7458
+ type: "button",
7459
+ onClick: () => setConfirmPrune(true),
7460
+ title: t("keep_only_retest_hint"),
7461
+ className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-text-lo",
7462
+ style: { background: "transparent", border: "none", cursor: "pointer" },
7463
+ children: [
7464
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Trash", size: 11 }),
7465
+ t("keep_only_retest", { n: noteCounts.fixed })
7466
+ ]
7467
+ }
7468
+ ) })
7280
7469
  ] });
7281
7470
  }
7282
7471
  function Section({
@@ -7812,249 +8001,9 @@ function CredentialsSection() {
7812
8001
  })
7813
8002
  ] });
7814
8003
  }
7815
- var keyOf = (id, path) => `${id}::${path}`;
7816
- var DEFAULT_LANE_COLOR = "#4D9CFF";
7817
- function Lane({
7818
- group,
7819
- checked,
7820
- toggle,
7821
- pick: pick2,
7822
- onWalkFrom
7823
- }) {
7824
- const { lang, t, guideFailed, evidenceByStep } = useQa();
7825
- const { id, color = DEFAULT_LANE_COLOR, steps: steps2 } = group;
7826
- const done = steps2.filter((s) => checked.has(keyOf(id, s.path))).length;
7827
- const pct = steps2.length > 0 ? Math.round(done / steps2.length * 100) : 0;
7828
- const uncoveredRedCount = steps2.filter(
7829
- (s) => s.risk === "red" && !checked.has(keyOf(id, s.path))
7830
- ).length;
7831
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-3 qa-elev-1", children: [
7832
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-2 qa-flex qa-items-center qa-gap-2", children: [
7833
- /* @__PURE__ */ jsxRuntime.jsx(
7834
- "span",
7835
- {
7836
- className: "qa-h-2.5 qa-w-2.5 qa-rounded-full",
7837
- style: { background: color }
7838
- }
7839
- ),
7840
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-sm qa-font-bold qa-text-hi", children: pick2(group.role) }),
7841
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-ms-auto qa-text-11 qa-font-medium qa-text-slate-400", children: [
7842
- done,
7843
- "/",
7844
- steps2.length
7845
- ] }),
7846
- uncoveredRedCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(
7847
- "span",
7848
- {
7849
- className: "qa-bg-danger-tint qa-text-danger qa-rounded qa-px-1 qa-text-10 qa-font-medium",
7850
- title: lang === "ar" ? `${uncoveredRedCount} \u0645\u0646\u0637\u0642\u0629 \u062D\u0645\u0631\u0627\u0621 \u063A\u064A\u0631 \u0645\u063A\u0637\u0627\u0629` : `${uncoveredRedCount} uncovered red zone(s)`,
7851
- children: lang === "ar" ? `\u0623\u062D\u0645\u0631: ${uncoveredRedCount}` : `red: ${uncoveredRedCount}`
7852
- }
7853
- )
7854
- ] }),
7855
- /* @__PURE__ */ jsxRuntime.jsx(
7856
- "div",
7857
- {
7858
- className: "qa-mb-3 qa-h-1.5 qa-overflow-hidden qa-rounded-full",
7859
- style: { background: `${color}22` },
7860
- children: /* @__PURE__ */ jsxRuntime.jsx(
7861
- "div",
7862
- {
7863
- className: "qa-h-full qa-rounded-full qa-transition-all",
7864
- style: { width: `${pct}%`, background: color }
7865
- }
7866
- )
7867
- }
7868
- ),
7869
- /* @__PURE__ */ jsxRuntime.jsxs("ol", { className: "qa-relative qa-ms-1.5", children: [
7870
- /* @__PURE__ */ jsxRuntime.jsx(
7871
- "span",
7872
- {
7873
- className: "qa-absolute qa-top-1 qa-bottom-0 qa-w-px",
7874
- style: { insetInlineStart: "7px", background: `${color}40`, bottom: "4px" }
7875
- }
7876
- ),
7877
- steps2.map((s, i) => {
7878
- const k = keyOf(id, s.path);
7879
- const on = checked.has(k);
7880
- const failed = guideFailed.has(k);
7881
- const evidence = evidenceByStep.get(k);
7882
- const evidenceCount = evidence ? evidence.length : 0;
7883
- const riskColor = s.risk ? RISK_COLORS[s.risk] : RISK_COLORS.none;
7884
- const dotTitle = !s.risk ? lang === "ar" ? "\u0644\u0645 \u064A\u062A\u0645 \u062A\u0642\u064A\u064A\u0645 \u0627\u0644\u0645\u062E\u0627\u0637\u0631 \u0628\u0639\u062F" : "not graded yet" : s.riskWhy ?? s.risk;
7885
- return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-relative qa-mb-2 qa-last-mb-0", children: [
7886
- /* @__PURE__ */ jsxRuntime.jsx(
7887
- "button",
7888
- {
7889
- onClick: () => onWalkFrom(k),
7890
- title: t("walk_take_me", { path: s.path }),
7891
- "aria-label": t("walk_take_me", { path: s.path }),
7892
- className: "qa-tap-icon qa-absolute qa-rounded-lg qa-text-mid qa-hover-text-slate-600",
7893
- style: {
7894
- top: 0,
7895
- insetInlineEnd: 0,
7896
- background: "transparent",
7897
- border: "none",
7898
- cursor: "pointer",
7899
- zIndex: 2
7900
- },
7901
- children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Play", size: 12 })
7902
- }
7903
- ),
7904
- /* @__PURE__ */ jsxRuntime.jsxs(
7905
- "button",
7906
- {
7907
- onClick: () => toggle(k),
7908
- className: `qa-flex qa-w-full qa-items-start qa-gap-2.5 qa-rounded-lg qa-p-1 qa-text-start qa-hover-bg-black-3${failed ? " qa-bg-danger-tint" : ""}`,
7909
- style: { background: failed ? void 0 : "transparent", border: "none", cursor: "pointer" },
7910
- children: [
7911
- /* @__PURE__ */ jsxRuntime.jsxs(
7912
- "span",
7913
- {
7914
- className: "qa-relative qa-z-1 qa-mt-0.5 qa-flex qa-h-4 qa-w-4 qa-shrink-0 qa-items-center qa-justify-center qa-rounded-full qa-border-2 qa-transition",
7915
- style: {
7916
- borderColor: failed ? "var(--qa-danger)" : color,
7917
- background: on ? color : failed ? "var(--qa-danger-tint)" : "var(--qa-surface-1)",
7918
- zIndex: 1
7919
- },
7920
- children: [
7921
- on && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 10, strokeWidth: 3, className: "qa-text-white" }),
7922
- !on && failed && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "AlertTriangle", size: 9, strokeWidth: 2.5, className: "qa-text-danger" })
7923
- ]
7924
- }
7925
- ),
7926
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-min-w-0", children: [
7927
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1", children: [
7928
- /* @__PURE__ */ jsxRuntime.jsx(
7929
- "code",
7930
- {
7931
- className: "qa-rounded qa-px-1 qa-text-11 qa-font-semibold qa-dir-ltr qa-text-hi",
7932
- style: {
7933
- background: failed ? "var(--qa-danger-tint)" : `${color}14`,
7934
- textDecoration: on ? "line-through" : "none",
7935
- opacity: on ? 0.55 : 1
7936
- },
7937
- children: s.path
7938
- }
7939
- ),
7940
- /* @__PURE__ */ jsxRuntime.jsx(
7941
- "span",
7942
- {
7943
- className: "qa-inline-block qa-rounded-full qa-shrink-0",
7944
- style: {
7945
- width: "6px",
7946
- height: "6px",
7947
- background: riskColor,
7948
- flexShrink: 0
7949
- },
7950
- title: dotTitle
7951
- }
7952
- )
7953
- ] }),
7954
- /* @__PURE__ */ jsxRuntime.jsx(
7955
- "span",
7956
- {
7957
- className: "qa-mt-0.5 qa-block qa-text-11 qa-leading-relaxed qa-text-slate-500",
7958
- style: { opacity: on ? 0.5 : 1 },
7959
- children: pick2(s.what)
7960
- }
7961
- ),
7962
- evidenceCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-mt-1 qa-inline-flex qa-items-center qa-rounded qa-bg-accent-tint qa-text-accent qa-px-1 qa-text-10 qa-font-medium", children: t("evidence_n", { n: evidenceCount }) }),
7963
- evidenceCount === 0 && on && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-mt-1 qa-inline-flex qa-items-center qa-rounded qa-bg-warn-tint qa-text-warn qa-px-1 qa-text-10 qa-font-medium", children: t("no_evidence") })
7964
- ] })
7965
- ]
7966
- }
7967
- )
7968
- ] }, `${k}-${i}`);
7969
- })
7970
- ] })
7971
- ] });
7972
- }
7973
- function GuideSection() {
7974
- const {
7975
- guideChecked,
7976
- toggleGuide,
7977
- t,
7978
- journey,
7979
- journeyIsGeneric,
7980
- pick: pick2,
7981
- lang,
7982
- startTestAlong,
7983
- startWalk,
7984
- testAlongSteps
7985
- } = useQa();
7986
- const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
7987
- const done = all.filter((k) => guideChecked.has(k)).length;
7988
- const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
7989
- const coverage = computeCoverage(journey, guideChecked);
7990
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-3", children: [
7991
- journeyIsGeneric && /* @__PURE__ */ jsxRuntime.jsx(
7992
- "p",
7993
- {
7994
- 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",
7995
- "data-qa-generic-plan": "true",
7996
- children: t("journey_generic")
7997
- }
7998
- ),
7999
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
8000
- 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: [
8001
- /* @__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" }),
8002
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-dir-ltr qa-font-semibold qa-text-hi", children: [
8003
- coverage.red.covered,
8004
- "/",
8005
- coverage.red.total,
8006
- " ",
8007
- lang === "ar" ? "\u0645\u063A\u0637\u0649" : "covered"
8008
- ] })
8009
- ] }),
8010
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-justify-between qa-text-sm qa-font-semibold qa-text-hi", children: [
8011
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("journey_title") }),
8012
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-dir-ltr", children: [
8013
- done,
8014
- "/",
8015
- all.length,
8016
- " \xB7 ",
8017
- pct,
8018
- "%"
8019
- ] })
8020
- ] }),
8021
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mt-2 qa-h-2 qa-overflow-hidden qa-rounded-full qa-bg-3", children: /* @__PURE__ */ jsxRuntime.jsx(
8022
- "div",
8023
- {
8024
- className: "qa-h-full qa-rounded-full qa-bg-accent qa-transition-all",
8025
- style: { width: `${pct}%` }
8026
- }
8027
- ) }),
8028
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mt-2 qa-flex qa-justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
8029
- "button",
8030
- {
8031
- type: "button",
8032
- onClick: startTestAlong,
8033
- disabled: all.length === 0,
8034
- className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-full qa-bg-accent qa-border-0 qa-cursor-pointer qa-px-3 qa-py-1 qa-text-xs qa-font-semibold qa-focus-ring",
8035
- children: [
8036
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Play", size: 13 }),
8037
- t("start_walkthrough")
8038
- ]
8039
- }
8040
- ) })
8041
- ] }),
8042
- journey.map((g) => /* @__PURE__ */ jsxRuntime.jsx(
8043
- Lane,
8044
- {
8045
- group: g,
8046
- checked: guideChecked,
8047
- toggle: toggleGuide,
8048
- onWalkFrom: (key) => {
8049
- const at = testAlongSteps.findIndex((step) => step.key === key);
8050
- startWalk("plan", at === -1 ? 0 : at);
8051
- },
8052
- pick: pick2
8053
- },
8054
- g.id
8055
- ))
8056
- ] });
8057
- }
8004
+
8005
+ // src/lib/features.ts
8006
+ var GUIDE_TAB_ENABLED = false;
8058
8007
  function isCoarsePointer() {
8059
8008
  if (typeof window === "undefined") return false;
8060
8009
  try {
@@ -8078,11 +8027,15 @@ function useCoarsePointer() {
8078
8027
  }, []);
8079
8028
  return coarse;
8080
8029
  }
8081
- var TABS = [
8030
+ var ALL_TABS = [
8082
8031
  { key: "notes", labelKey: "tab_notes", icon: "StickyNote" },
8083
8032
  { key: "logins", labelKey: "tab_logins", icon: "KeyRound" },
8084
8033
  { key: "guide", labelKey: "tab_guide", icon: "Map" }
8085
8034
  ];
8035
+ var TABS = ALL_TABS.filter((tab) => tab.key !== "guide" || GUIDE_TAB_ENABLED);
8036
+ function visibleTab(tab) {
8037
+ return tab === "guide" && !GUIDE_TAB_ENABLED ? "notes" : tab;
8038
+ }
8086
8039
  function todayName() {
8087
8040
  return `qa-notes-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
8088
8041
  }
@@ -8117,7 +8070,7 @@ var PANEL_TRANSITION_WITH_LIFT = "opacity 200ms cubic-bezier(0.4,0,0.2,1), trans
8117
8070
  function QaPanel() {
8118
8071
  const {
8119
8072
  isOpen,
8120
- activeTab,
8073
+ activeTab: storedTab,
8121
8074
  setActiveTab,
8122
8075
  notes,
8123
8076
  exportZip,
@@ -8148,6 +8101,7 @@ function QaPanel() {
8148
8101
  panelCollapsed,
8149
8102
  setPanelCollapsed
8150
8103
  } = useQa();
8104
+ const activeTab = visibleTab(storedTab);
8151
8105
  const [confirmClear, setConfirmClear] = React.useState(false);
8152
8106
  const [naming, setNaming] = React.useState(false);
8153
8107
  const [filename, setFilename] = React.useState("");
@@ -8515,7 +8469,7 @@ function QaPanel() {
8515
8469
  ) })
8516
8470
  ] }),
8517
8471
  !simpleMode && activeTab === "logins" && /* @__PURE__ */ jsxRuntime.jsx(CredentialsSection, {}),
8518
- !simpleMode && activeTab === "guide" && /* @__PURE__ */ jsxRuntime.jsx(GuideSection, {})
8472
+ !simpleMode && GUIDE_TAB_ENABLED
8519
8473
  ] })
8520
8474
  ] }),
8521
8475
  settingsOpen && /* @__PURE__ */ jsxRuntime.jsx(SettingsSheet, { onClose: () => setSettingsOpen(false) }),
@@ -10374,5 +10328,5 @@ function Qapture({ config }) {
10374
10328
  exports.Qapture = Qapture;
10375
10329
  exports.deleteQaDatabase = deleteQaDatabase;
10376
10330
  exports.initQaStudio = initQaStudio;
10377
- //# sourceMappingURL=chunk-WU7QMHPK.cjs.map
10378
- //# sourceMappingURL=chunk-WU7QMHPK.cjs.map
10331
+ //# sourceMappingURL=chunk-4LMLW73U.cjs.map
10332
+ //# sourceMappingURL=chunk-4LMLW73U.cjs.map
package/dist/index.cjs CHANGED
@@ -1,24 +1,24 @@
1
1
  'use strict';
2
2
 
3
- var chunkWU7QMHPK_cjs = require('./chunk-WU7QMHPK.cjs');
3
+ var chunk4LMLW73U_cjs = require('./chunk-4LMLW73U.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "QaStudio", {
8
8
  enumerable: true,
9
- get: function () { return chunkWU7QMHPK_cjs.Qapture; }
9
+ get: function () { return chunk4LMLW73U_cjs.Qapture; }
10
10
  });
11
11
  Object.defineProperty(exports, "Qapture", {
12
12
  enumerable: true,
13
- get: function () { return chunkWU7QMHPK_cjs.Qapture; }
13
+ get: function () { return chunk4LMLW73U_cjs.Qapture; }
14
14
  });
15
15
  Object.defineProperty(exports, "deleteQaDatabase", {
16
16
  enumerable: true,
17
- get: function () { return chunkWU7QMHPK_cjs.deleteQaDatabase; }
17
+ get: function () { return chunk4LMLW73U_cjs.deleteQaDatabase; }
18
18
  });
19
19
  Object.defineProperty(exports, "initQaStudio", {
20
20
  enumerable: true,
21
- get: function () { return chunkWU7QMHPK_cjs.initQaStudio; }
21
+ get: function () { return chunk4LMLW73U_cjs.initQaStudio; }
22
22
  });
23
23
  //# sourceMappingURL=index.cjs.map
24
24
  //# sourceMappingURL=index.cjs.map