@mhosaic/feedback 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -555,6 +555,14 @@ function installCapture(options = {}) {
555
555
  // src/widget/i18n.ts
556
556
  var DEFAULT_STRINGS = {
557
557
  "fab.label": "Send feedback",
558
+ "page.badge.aria": "Feedback \u2014 {n} open on this page",
559
+ "page.badge.aria.one": "Feedback \u2014 1 open on this page",
560
+ "page.strip.text": "{n} open reports on this page",
561
+ "page.strip.text.one": "1 open report on this page",
562
+ "page.strip.view": "View",
563
+ "page.peek.title": "On this page",
564
+ "page.peek.viewAll": "View all {n}",
565
+ "page.peek.viewAll.one": "View 1 open report",
558
566
  "form.title": "Send feedback",
559
567
  "form.description.label": "What happened?",
560
568
  "form.description.placeholder": "Describe the issue or idea in one or two sentences.",
@@ -639,6 +647,11 @@ var DEFAULT_STRINGS = {
639
647
  "board.scope.project": "Project reports",
640
648
  "board.scope.mine": "Your reports",
641
649
  "board.scope.thisPage": "This page",
650
+ "board.scope.allPages": "All pages",
651
+ "board.scope.onThisPage": "on this page",
652
+ "board.list.count.page": "{n} of {total} \xB7 this page",
653
+ "board.list.empty.page.title": "No feedback on this page yet.",
654
+ "board.list.empty.page.description": "Be the first to report something here \u2014 or browse the whole project.",
642
655
  "board.back": "Back",
643
656
  "changelog.empty.title": "Nothing resolved yet",
644
657
  "changelog.empty.body": "Once a report you sent is fixed it will appear here, grouped by week.",
@@ -706,6 +719,14 @@ var DEFAULT_STRINGS = {
706
719
  };
707
720
  var FRENCH_STRINGS = {
708
721
  "fab.label": "Envoyer un commentaire",
722
+ "page.badge.aria": "Retours \u2014 {n} ouverts sur cette page",
723
+ "page.badge.aria.one": "Retours \u2014 1 ouvert sur cette page",
724
+ "page.strip.text": "{n} retours ouverts sur cette page",
725
+ "page.strip.text.one": "1 retour ouvert sur cette page",
726
+ "page.strip.view": "Voir",
727
+ "page.peek.title": "Sur cette page",
728
+ "page.peek.viewAll": "Voir les {n}",
729
+ "page.peek.viewAll.one": "Voir 1 retour ouvert",
709
730
  "form.title": "Envoyer un commentaire",
710
731
  "form.description.label": "Qu\u2019est-il arriv\xE9 ?",
711
732
  "form.description.placeholder": "D\xE9crivez le probl\xE8me ou l\u2019id\xE9e en une ou deux phrases.",
@@ -790,6 +811,11 @@ var FRENCH_STRINGS = {
790
811
  "board.scope.project": "Rapports du projet",
791
812
  "board.scope.mine": "Vos rapports",
792
813
  "board.scope.thisPage": "Cette page",
814
+ "board.scope.allPages": "Toutes les pages",
815
+ "board.scope.onThisPage": "sur cette page",
816
+ "board.list.count.page": "{n} sur {total} \xB7 cette page",
817
+ "board.list.empty.page.title": "Aucun retour sur cette page pour l\u2019instant.",
818
+ "board.list.empty.page.description": "Soyez le premier \xE0 signaler quelque chose ici \u2014 ou parcourez tout le projet.",
793
819
  "board.back": "Retour",
794
820
  "changelog.empty.title": "Rien de r\xE9solu pour l\u2019instant",
795
821
  "changelog.empty.body": "Quand un rapport que vous avez envoy\xE9 est corrig\xE9, il appara\xEEtra ici, regroup\xE9 par semaine.",
@@ -874,7 +900,7 @@ function resolveStrings(overrides, options = {}) {
874
900
 
875
901
  // src/widget/mount.tsx
876
902
  import { h, render } from "preact";
877
- import { useCallback, useEffect as useEffect8, useState as useState7 } from "preact/hooks";
903
+ import { useCallback as useCallback2, useEffect as useEffect10, useRef as useRef7, useState as useState9 } from "preact/hooks";
878
904
 
879
905
  // src/widget/currentPage.ts
880
906
  function currentPagePath(opts) {
@@ -928,6 +954,47 @@ function saveBoardView(externalId, view) {
928
954
  }
929
955
  }
930
956
 
957
+ // src/widget/poll.ts
958
+ var MAX_BACKOFF_MULTIPLIER = 10;
959
+ function startPoll(tick, intervalMs) {
960
+ let stopped = false;
961
+ let parked = false;
962
+ let failures = 0;
963
+ let timer = null;
964
+ const doc = typeof document !== "undefined" ? document : null;
965
+ const delayMs = () => Math.min(intervalMs * 2 ** failures, intervalMs * MAX_BACKOFF_MULTIPLIER);
966
+ const run = async () => {
967
+ if (stopped) return;
968
+ if (doc?.hidden) {
969
+ parked = true;
970
+ return;
971
+ }
972
+ let ok = false;
973
+ try {
974
+ ok = await tick();
975
+ } catch {
976
+ ok = false;
977
+ }
978
+ if (stopped) return;
979
+ failures = ok ? 0 : failures + 1;
980
+ timer = setTimeout(() => void run(), delayMs());
981
+ };
982
+ const onVisibilityChange = () => {
983
+ if (stopped || !parked || doc?.hidden) return;
984
+ parked = false;
985
+ void run();
986
+ };
987
+ doc?.addEventListener("visibilitychange", onVisibilityChange);
988
+ void run();
989
+ return {
990
+ stop() {
991
+ stopped = true;
992
+ if (timer) clearTimeout(timer);
993
+ doc?.removeEventListener("visibilitychange", onVisibilityChange);
994
+ }
995
+ };
996
+ }
997
+
931
998
  // src/widget/ReportDetailView.tsx
932
999
  import { useEffect, useRef, useState } from "preact/hooks";
933
1000
 
@@ -1019,24 +1086,23 @@ function ReportDetailView({
1019
1086
  const fetchDetail = async () => {
1020
1087
  try {
1021
1088
  const next = await api.getReport(reportId, externalId);
1022
- if (!mountedRef.current) return;
1089
+ if (!mountedRef.current) return true;
1023
1090
  setDetail(next);
1024
1091
  setError(null);
1092
+ return true;
1025
1093
  } catch (err) {
1026
1094
  if (typeof console !== "undefined") console.warn("[mhosaic] getReport:", err);
1027
- if (!mountedRef.current) return;
1095
+ if (!mountedRef.current) return false;
1028
1096
  setError("load_failed");
1097
+ return false;
1029
1098
  }
1030
1099
  };
1031
1100
  useEffect(() => {
1032
1101
  mountedRef.current = true;
1033
- void fetchDetail();
1034
- const timer = setInterval(() => {
1035
- void fetchDetail();
1036
- }, POLL_MS);
1102
+ const poll = startPoll(fetchDetail, POLL_MS);
1037
1103
  return () => {
1038
1104
  mountedRef.current = false;
1039
- clearInterval(timer);
1105
+ poll.stop();
1040
1106
  };
1041
1107
  }, [reportId, externalId]);
1042
1108
  const handleSend = async () => {
@@ -1378,7 +1444,13 @@ var STATUSES = [
1378
1444
  ];
1379
1445
  var TYPES = ["bug", "feature", "question", "praise", "typo"];
1380
1446
  var SEVERITIES = ["blocker", "high", "medium", "low"];
1381
- function BoardView({ api, externalId, strings, getCurrentPage }) {
1447
+ function BoardView({
1448
+ api,
1449
+ externalId,
1450
+ strings,
1451
+ getCurrentPage,
1452
+ initialSelectedId
1453
+ }) {
1382
1454
  const [filters, setFilters] = useState2(() => loadBoardView(externalId));
1383
1455
  const [thisPage, setThisPage] = useState2(true);
1384
1456
  useEffect2(() => {
@@ -1388,7 +1460,7 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1388
1460
  const [kpis, setKpis] = useState2(null);
1389
1461
  const [loading, setLoading] = useState2(true);
1390
1462
  const [error, setError] = useState2(null);
1391
- const [selectedId, setSelectedId] = useState2(null);
1463
+ const [selectedId, setSelectedId] = useState2(initialSelectedId ?? null);
1392
1464
  const [detailKey, setDetailKey] = useState2(0);
1393
1465
  const [reloadTick, setReloadTick] = useState2(0);
1394
1466
  const activeFilterCount = useMemo(() => {
@@ -1405,30 +1477,29 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1405
1477
  );
1406
1478
  useEffect2(() => {
1407
1479
  let cancelled = false;
1408
- let timer = null;
1409
- const tick = async () => {
1480
+ setLoading(true);
1481
+ const poll = startPoll(async () => {
1410
1482
  try {
1411
1483
  const [list, k] = await Promise.all([
1412
1484
  api.listBoard(externalId, fetchFilters),
1413
1485
  api.fetchBoardKpis(externalId, fetchFilters)
1414
1486
  ]);
1415
- if (cancelled) return;
1487
+ if (cancelled) return true;
1416
1488
  setPage(list);
1417
1489
  setKpis(k);
1418
1490
  setError(null);
1491
+ return true;
1419
1492
  } catch (e) {
1420
- if (cancelled) return;
1493
+ if (cancelled) return false;
1421
1494
  setError(e instanceof Error ? e.message : String(e));
1495
+ return false;
1422
1496
  } finally {
1423
1497
  if (!cancelled) setLoading(false);
1424
- if (!cancelled) timer = setTimeout(tick, POLL_MS2);
1425
1498
  }
1426
- };
1427
- setLoading(true);
1428
- void tick();
1499
+ }, POLL_MS2);
1429
1500
  return () => {
1430
1501
  cancelled = true;
1431
- if (timer) clearTimeout(timer);
1502
+ poll.stop();
1432
1503
  };
1433
1504
  }, [api, externalId, JSON.stringify(fetchFilters)]);
1434
1505
  const selectedRow = useMemo(() => {
@@ -1440,7 +1511,7 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1440
1511
  setDetailKey((k) => k + 1);
1441
1512
  };
1442
1513
  return /* @__PURE__ */ jsxs3("div", { class: "board-view", children: [
1443
- /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis }),
1514
+ /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis, thisPage }),
1444
1515
  /* @__PURE__ */ jsx3(
1445
1516
  BoardFilters,
1446
1517
  {
@@ -1471,7 +1542,14 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1471
1542
  )
1472
1543
  ] }),
1473
1544
  !error && loading && !page && /* @__PURE__ */ jsx3(BoardListSkeleton, {}),
1474
- !error && page && page.results.length === 0 && !loading && /* @__PURE__ */ jsx3(BoardEmpty, { strings }),
1545
+ !error && page && page.results.length === 0 && !loading && /* @__PURE__ */ jsx3(
1546
+ BoardEmpty,
1547
+ {
1548
+ strings,
1549
+ thisPage,
1550
+ onAllPages: () => setThisPage(false)
1551
+ }
1552
+ ),
1475
1553
  !error && page && page.results.length > 0 && /* @__PURE__ */ jsx3(
1476
1554
  BoardList,
1477
1555
  {
@@ -1479,7 +1557,8 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1479
1557
  selectedId,
1480
1558
  onPick: onPickRow,
1481
1559
  strings,
1482
- total: page.count
1560
+ total: page.count,
1561
+ thisPage
1483
1562
  }
1484
1563
  )
1485
1564
  ] }),
@@ -1504,9 +1583,10 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1504
1583
  }
1505
1584
  function BoardHeader({
1506
1585
  strings,
1507
- kpis
1586
+ kpis,
1587
+ thisPage
1508
1588
  }) {
1509
- const scopeLabel = kpis?.scope === "project" ? strings["board.scope.project"] : strings["board.scope.mine"];
1589
+ const scopeLabel = thisPage ? strings["board.scope.onThisPage"] : kpis?.scope === "project" ? strings["board.scope.project"] : strings["board.scope.mine"];
1510
1590
  return /* @__PURE__ */ jsxs3("header", { class: "board-header", children: [
1511
1591
  /* @__PURE__ */ jsxs3("div", { class: "board-header-title", children: [
1512
1592
  /* @__PURE__ */ jsx3("span", { class: "board-header-emoji", "aria-hidden": "true", children: "\u{1F4CB}" }),
@@ -1601,6 +1681,28 @@ function BoardFilters({
1601
1681
  };
1602
1682
  const clear = () => onChange({});
1603
1683
  return /* @__PURE__ */ jsxs3("div", { class: "board-filters", children: [
1684
+ /* @__PURE__ */ jsxs3("div", { class: "board-scope", role: "group", "aria-label": strings["board.scope.thisPage"], children: [
1685
+ /* @__PURE__ */ jsx3(
1686
+ "button",
1687
+ {
1688
+ type: "button",
1689
+ class: `board-scope-btn ${thisPage ? "is-active" : ""}`,
1690
+ "aria-pressed": thisPage,
1691
+ onClick: () => !thisPage && onToggleThisPage(),
1692
+ children: strings["board.scope.thisPage"]
1693
+ }
1694
+ ),
1695
+ /* @__PURE__ */ jsx3(
1696
+ "button",
1697
+ {
1698
+ type: "button",
1699
+ class: `board-scope-btn ${!thisPage ? "is-active" : ""}`,
1700
+ "aria-pressed": !thisPage,
1701
+ onClick: () => thisPage && onToggleThisPage(),
1702
+ children: strings["board.scope.allPages"]
1703
+ }
1704
+ )
1705
+ ] }),
1604
1706
  /* @__PURE__ */ jsx3(
1605
1707
  "select",
1606
1708
  {
@@ -1671,19 +1773,6 @@ function BoardFilters({
1671
1773
  ),
1672
1774
  /* @__PURE__ */ jsx3("span", { children: strings["board.filter.mine"] })
1673
1775
  ] }),
1674
- /* @__PURE__ */ jsxs3(
1675
- "button",
1676
- {
1677
- type: "button",
1678
- class: `board-chip ${thisPage ? "board-chip--on" : ""}`,
1679
- onClick: onToggleThisPage,
1680
- children: [
1681
- "\u{1F4CD} ",
1682
- strings["board.scope.thisPage"],
1683
- thisPage ? " \u2715" : ""
1684
- ]
1685
- }
1686
- ),
1687
1776
  activeCount > 0 && /* @__PURE__ */ jsxs3("button", { type: "button", class: "board-filter-clear", onClick: clear, children: [
1688
1777
  /* @__PURE__ */ jsx3(CloseIcon, {}),
1689
1778
  strings["board.filter.clear"]
@@ -1695,10 +1784,11 @@ function BoardList({
1695
1784
  selectedId,
1696
1785
  onPick,
1697
1786
  strings,
1698
- total
1787
+ total,
1788
+ thisPage
1699
1789
  }) {
1700
1790
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1701
- /* @__PURE__ */ jsx3("div", { class: "board-list-count", children: strings["board.list.count"].replace("{n}", String(rows.length)).replace("{total}", String(total)) }),
1791
+ /* @__PURE__ */ jsx3("div", { class: "board-list-count", children: strings[thisPage ? "board.list.count.page" : "board.list.count"].replace("{n}", String(rows.length)).replace("{total}", String(total)) }),
1702
1792
  /* @__PURE__ */ jsx3("ul", { class: "board-list", role: "list", children: rows.map((row) => /* @__PURE__ */ jsx3(
1703
1793
  BoardRowCard,
1704
1794
  {
@@ -1744,11 +1834,16 @@ function BoardRowCard({
1744
1834
  }
1745
1835
  ) });
1746
1836
  }
1747
- function BoardEmpty({ strings }) {
1837
+ function BoardEmpty({
1838
+ strings,
1839
+ thisPage,
1840
+ onAllPages
1841
+ }) {
1748
1842
  return /* @__PURE__ */ jsxs3("div", { class: "board-empty", children: [
1749
1843
  /* @__PURE__ */ jsx3(EmptyIllustration, {}),
1750
- /* @__PURE__ */ jsx3("h3", { children: strings["board.list.empty.title"] }),
1751
- /* @__PURE__ */ jsx3("p", { children: strings["board.list.empty.description"] })
1844
+ /* @__PURE__ */ jsx3("h3", { children: strings[thisPage ? "board.list.empty.page.title" : "board.list.empty.title"] }),
1845
+ /* @__PURE__ */ jsx3("p", { children: strings[thisPage ? "board.list.empty.page.description" : "board.list.empty.description"] }),
1846
+ thisPage && /* @__PURE__ */ jsx3("button", { type: "button", class: "btn btn--ghost", onClick: onAllPages, children: strings["board.scope.allPages"] })
1752
1847
  ] });
1753
1848
  }
1754
1849
  function BoardListSkeleton() {
@@ -1901,26 +1996,25 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
1901
1996
  setError(null);
1902
1997
  try {
1903
1998
  const next = await api.listChangelog(externalId);
1904
- if (!mountedRef.current) return;
1999
+ if (!mountedRef.current) return true;
1905
2000
  setRows(next);
2001
+ return true;
1906
2002
  } catch (err) {
1907
2003
  if (typeof console !== "undefined")
1908
2004
  console.warn("[mhosaic] listChangelog:", err);
1909
- if (!mountedRef.current) return;
2005
+ if (!mountedRef.current) return false;
1910
2006
  setError(strings["mine.error"]);
2007
+ return false;
1911
2008
  } finally {
1912
2009
  if (mountedRef.current) setRefreshing(false);
1913
2010
  }
1914
2011
  };
1915
2012
  useEffect3(() => {
1916
2013
  mountedRef.current = true;
1917
- void fetchRows();
1918
- const timer = setInterval(() => {
1919
- void fetchRows();
1920
- }, POLL_MS3);
2014
+ const poll = startPoll(fetchRows, POLL_MS3);
1921
2015
  return () => {
1922
2016
  mountedRef.current = false;
1923
- clearInterval(timer);
2017
+ poll.stop();
1924
2018
  };
1925
2019
  }, [externalId]);
1926
2020
  const groups = useMemo2(() => rows ? groupRowsByWeek(rows) : [], [rows]);
@@ -1992,8 +2086,11 @@ function BugIcon() {
1992
2086
  }
1993
2087
  );
1994
2088
  }
1995
- function Fab({ label, onClick }) {
1996
- return /* @__PURE__ */ jsx6("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: /* @__PURE__ */ jsx6(BugIcon, {}) });
2089
+ function Fab({ label, onClick, count }) {
2090
+ return /* @__PURE__ */ jsxs6("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: [
2091
+ /* @__PURE__ */ jsx6(BugIcon, {}),
2092
+ count !== void 0 && count > 0 && /* @__PURE__ */ jsx6("span", { class: "fab-badge", "aria-hidden": "true", children: count > 9 ? "9+" : String(count) })
2093
+ ] });
1997
2094
  }
1998
2095
 
1999
2096
  // src/widget/Form.tsx
@@ -2885,25 +2982,24 @@ function MineList({ api, externalId, strings, onSelect }) {
2885
2982
  setError(null);
2886
2983
  try {
2887
2984
  const next = await api.listMine(externalId);
2888
- if (!mountedRef.current) return;
2985
+ if (!mountedRef.current) return true;
2889
2986
  setRows(next);
2987
+ return true;
2890
2988
  } catch (err) {
2891
2989
  if (typeof console !== "undefined") console.warn("[mhosaic] listMine:", err);
2892
- if (!mountedRef.current) return;
2990
+ if (!mountedRef.current) return false;
2893
2991
  setError(strings["mine.error"]);
2992
+ return false;
2894
2993
  } finally {
2895
2994
  if (mountedRef.current) setRefreshing(false);
2896
2995
  }
2897
2996
  };
2898
2997
  useEffect6(() => {
2899
2998
  mountedRef.current = true;
2900
- void fetchRows();
2901
- const timer = setInterval(() => {
2902
- void fetchRows();
2903
- }, POLL_MS4);
2999
+ const poll = startPoll(fetchRows, POLL_MS4);
2904
3000
  return () => {
2905
3001
  mountedRef.current = false;
2906
- clearInterval(timer);
3002
+ poll.stop();
2907
3003
  };
2908
3004
  }, [externalId]);
2909
3005
  const isEmpty = rows !== null && rows.length === 0;
@@ -3000,6 +3096,136 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3000
3096
  );
3001
3097
  }
3002
3098
 
3099
+ // src/widget/PageActivityStrip.tsx
3100
+ import { jsx as jsx12, jsxs as jsxs12 } from "preact/jsx-runtime";
3101
+ function PageActivityStrip({
3102
+ open,
3103
+ strings,
3104
+ onView
3105
+ }) {
3106
+ if (open === 0) return null;
3107
+ const text = open === 1 ? strings["page.strip.text.one"] : strings["page.strip.text"].replace("{n}", String(open));
3108
+ return /* @__PURE__ */ jsxs12("div", { class: "page-strip", children: [
3109
+ /* @__PURE__ */ jsx12("span", { children: text }),
3110
+ /* @__PURE__ */ jsx12("button", { type: "button", class: "page-strip-view", onClick: onView, children: strings["page.strip.view"] })
3111
+ ] });
3112
+ }
3113
+
3114
+ // src/widget/PagePeekPanel.tsx
3115
+ import { useEffect as useEffect8, useState as useState7 } from "preact/hooks";
3116
+ import { jsx as jsx13, jsxs as jsxs13 } from "preact/jsx-runtime";
3117
+ function PagePeekPanel({
3118
+ api,
3119
+ externalId,
3120
+ strings,
3121
+ getCurrentPage,
3122
+ open,
3123
+ onViewAll,
3124
+ onPickRow,
3125
+ onSend
3126
+ }) {
3127
+ const [rows, setRows] = useState7(null);
3128
+ const [failed, setFailed] = useState7(false);
3129
+ useEffect8(() => {
3130
+ let cancelled = false;
3131
+ const pagePath = currentPagePath(getCurrentPage !== void 0 ? { getCurrentPage } : {});
3132
+ api.listBoard(externalId, {
3133
+ pagePath,
3134
+ status: ["new", "in_progress", "awaiting_validation"]
3135
+ }).then((page) => {
3136
+ if (!cancelled) setRows(page.results.slice(0, 5));
3137
+ }).catch(() => {
3138
+ if (!cancelled) setFailed(true);
3139
+ });
3140
+ return () => {
3141
+ cancelled = true;
3142
+ };
3143
+ }, [api, externalId]);
3144
+ return /* @__PURE__ */ jsxs13("div", { class: "page-peek", role: "region", "aria-label": strings["page.peek.title"], children: [
3145
+ /* @__PURE__ */ jsx13("div", { class: "page-peek-title", children: strings["page.peek.title"] }),
3146
+ rows === null && !failed && /* @__PURE__ */ jsxs13("div", { class: "page-peek-skeleton", "aria-hidden": "true", children: [
3147
+ /* @__PURE__ */ jsx13("div", {}),
3148
+ " ",
3149
+ /* @__PURE__ */ jsx13("div", {})
3150
+ ] }),
3151
+ rows !== null && rows.map((row) => /* @__PURE__ */ jsxs13(
3152
+ "button",
3153
+ {
3154
+ type: "button",
3155
+ class: "page-peek-row",
3156
+ onClick: () => onPickRow?.(row.id),
3157
+ children: [
3158
+ /* @__PURE__ */ jsx13("span", { class: `page-peek-dot status-${row.status}`, "aria-hidden": "true" }),
3159
+ /* @__PURE__ */ jsx13("span", { class: "page-peek-desc", children: row.description })
3160
+ ]
3161
+ },
3162
+ row.id
3163
+ )),
3164
+ /* @__PURE__ */ jsxs13("div", { class: "page-peek-footer", children: [
3165
+ /* @__PURE__ */ jsx13("button", { type: "button", class: "page-peek-viewall", onClick: onViewAll, children: open === 1 ? strings["page.peek.viewAll.one"] : strings["page.peek.viewAll"].replace("{n}", String(open)) }),
3166
+ /* @__PURE__ */ jsx13("button", { type: "button", class: "page-peek-send", onClick: onSend, children: strings["fab.label"] })
3167
+ ] })
3168
+ ] });
3169
+ }
3170
+
3171
+ // src/widget/pageSignal.ts
3172
+ import { useCallback, useEffect as useEffect9, useState as useState8 } from "preact/hooks";
3173
+ var TTL_MS = 6e4;
3174
+ function computeOpenCount(kpis) {
3175
+ const s = kpis.by_status;
3176
+ return (s.new ?? 0) + (s.in_progress ?? 0) + (s.awaiting_validation ?? 0);
3177
+ }
3178
+ var cache = /* @__PURE__ */ new Map();
3179
+ var inflight = /* @__PURE__ */ new Map();
3180
+ function invalidatePageSignal() {
3181
+ cache.clear();
3182
+ }
3183
+ function fetchOpen(api, externalId, path) {
3184
+ const key = `${externalId}|${path}`;
3185
+ const hit = cache.get(key);
3186
+ if (hit && Date.now() - hit.fetchedAt < TTL_MS) return Promise.resolve(hit.open);
3187
+ const pending = inflight.get(key);
3188
+ if (pending) return pending;
3189
+ const p = api.fetchBoardKpis(externalId, { pagePath: path }).then((kpis) => {
3190
+ const open = computeOpenCount(kpis);
3191
+ cache.set(key, { open, fetchedAt: Date.now() });
3192
+ return open;
3193
+ }).finally(() => inflight.delete(key));
3194
+ inflight.set(key, p);
3195
+ return p;
3196
+ }
3197
+ function usePageOpenCount(opts) {
3198
+ const { api, externalId, getCurrentPage, enabled } = opts;
3199
+ const [open, setOpen] = useState8(0);
3200
+ const [tick, setTick] = useState8(0);
3201
+ const refresh = useCallback(() => {
3202
+ invalidatePageSignal();
3203
+ setTick((t) => t + 1);
3204
+ }, []);
3205
+ useEffect9(() => {
3206
+ if (!enabled || !api || !externalId) {
3207
+ setOpen(0);
3208
+ return;
3209
+ }
3210
+ let cancelled = false;
3211
+ const load = () => {
3212
+ const path = currentPagePath(getCurrentPage !== void 0 ? { getCurrentPage } : {});
3213
+ fetchOpen(api, externalId, path).then((n) => {
3214
+ if (!cancelled) setOpen(n);
3215
+ }).catch(() => {
3216
+ if (!cancelled) setOpen(0);
3217
+ });
3218
+ };
3219
+ load();
3220
+ const unsub = onLocationChange(load);
3221
+ return () => {
3222
+ cancelled = true;
3223
+ unsub();
3224
+ };
3225
+ }, [api, externalId, enabled, tick]);
3226
+ return { open, refresh };
3227
+ }
3228
+
3003
3229
  // src/widget/styles.ts
3004
3230
  var WIDGET_STYLES = `
3005
3231
  :host {
@@ -3074,10 +3300,20 @@ var WIDGET_STYLES = `
3074
3300
  soft-black background with a light steel-blue lucide-bug glyph;
3075
3301
  custom SVG inlined (no emoji \u2014 emoji renders inconsistently across
3076
3302
  OSes and can't inherit color). Two-layer elevation, scale-on-press. */
3077
- .fab {
3303
+ .fab-area {
3078
3304
  position: fixed;
3079
3305
  bottom: 24px;
3080
3306
  right: 24px;
3307
+ /* No z-index on purpose: the modal .backdrop/.modal are later siblings in
3308
+ the shadow root, so DOM order must keep them painting above the FAB \u2014
3309
+ an explicit z-index here would trap the FAB on top of the open modal. */
3310
+ }
3311
+ .fab-area .fab {
3312
+ position: relative;
3313
+ right: auto;
3314
+ bottom: auto;
3315
+ }
3316
+ .fab {
3081
3317
  width: 48px;
3082
3318
  height: 48px;
3083
3319
  border-radius: 999px;
@@ -3121,9 +3357,43 @@ var WIDGET_STYLES = `
3121
3357
  0 4px 12px rgba(0, 0, 0, 0.32),
3122
3358
  0 2px 4px rgba(0, 0, 0, 0.40); }
3123
3359
  }
3360
+
3361
+ /* Page-activity badge \u2014 absolute so it never shifts the FAB's layout. */
3362
+ .fab-badge {
3363
+ position: absolute;
3364
+ top: -2px;
3365
+ right: -2px;
3366
+ min-width: 16px;
3367
+ height: 16px;
3368
+ padding: 0 4px;
3369
+ box-sizing: border-box;
3370
+ border-radius: 999px;
3371
+ background: var(--mfb-accent);
3372
+ color: #fff;
3373
+ font-size: 10px;
3374
+ font-weight: 700;
3375
+ line-height: 16px;
3376
+ text-align: center;
3377
+ box-shadow: 0 0 0 2px var(--mfb-fab-bg);
3378
+ animation: mfb-badge-in 150ms ease-out;
3379
+ }
3380
+ @keyframes mfb-badge-in {
3381
+ from {
3382
+ opacity: 0;
3383
+ transform: scale(0.6);
3384
+ }
3385
+ to {
3386
+ opacity: 1;
3387
+ transform: scale(1);
3388
+ }
3389
+ }
3390
+
3124
3391
  @media (prefers-reduced-motion: reduce) {
3125
3392
  .fab { transition: none; }
3126
3393
  .fab:hover, .fab:active { transform: none; }
3394
+ .fab-badge {
3395
+ animation: none;
3396
+ }
3127
3397
  }
3128
3398
 
3129
3399
  /* Backdrop \u2014 fade in with a slight blur for depth. The blur gives the
@@ -4470,6 +4740,59 @@ var WIDGET_STYLES = `
4470
4740
  background: var(--mfb-surface);
4471
4741
  }
4472
4742
 
4743
+ /* Scope segmented control \u2014 [This page | All pages], front of the filter row. */
4744
+ .board-scope {
4745
+ display: inline-flex;
4746
+ border: 1px solid var(--mfb-border);
4747
+ border-radius: var(--mfb-radius);
4748
+ overflow: hidden;
4749
+ flex: none;
4750
+ }
4751
+ .board-scope-btn {
4752
+ border: none;
4753
+ background: none;
4754
+ padding: 6px 10px;
4755
+ font: inherit;
4756
+ font-size: var(--mfb-text-sm);
4757
+ color: var(--mfb-text-muted);
4758
+ cursor: pointer;
4759
+ }
4760
+ .board-scope-btn.is-active {
4761
+ background: color-mix(in srgb, var(--mfb-accent) 14%, transparent);
4762
+ color: var(--mfb-accent);
4763
+ font-weight: 600;
4764
+ }
4765
+ .board-scope-btn:focus-visible {
4766
+ outline: 2px solid var(--mfb-accent);
4767
+ outline-offset: -2px;
4768
+ }
4769
+
4770
+ /* Send-tab page-activity strip \u2014 one line, muted, non-blocking. */
4771
+ .page-strip {
4772
+ display: flex;
4773
+ align-items: center;
4774
+ justify-content: space-between;
4775
+ gap: var(--mfb-space-3);
4776
+ margin: var(--mfb-space-3) 0 0;
4777
+ padding: var(--mfb-space-2) var(--mfb-space-3);
4778
+ border-radius: var(--mfb-radius);
4779
+ background: var(--mfb-surface-2);
4780
+ color: var(--mfb-text-muted);
4781
+ font-size: var(--mfb-text-sm);
4782
+ }
4783
+ .page-strip-view {
4784
+ border: none;
4785
+ background: none;
4786
+ padding: 0;
4787
+ color: var(--mfb-accent);
4788
+ font-size: var(--mfb-text-sm);
4789
+ font-weight: 600;
4790
+ cursor: pointer;
4791
+ }
4792
+ .page-strip-view:hover {
4793
+ text-decoration: underline;
4794
+ }
4795
+
4473
4796
  /* Master/detail layout \u2014 two-column at >=900px, stacked below. */
4474
4797
  .board-body {
4475
4798
  display: grid;
@@ -4698,10 +5021,90 @@ var WIDGET_STYLES = `
4698
5021
  @media (prefers-reduced-motion: reduce) {
4699
5022
  .skeleton-line { animation: none; }
4700
5023
  }
5024
+
5025
+ /* Hover peek panel \u2014 desktop-only ambient surface listing this page's
5026
+ open reports above the FAB (spec \xA73.2). Non-modal; never renders while
5027
+ the modal itself is open. */
5028
+ .page-peek {
5029
+ position: absolute;
5030
+ right: 0;
5031
+ bottom: 60px;
5032
+ width: 300px;
5033
+ padding: var(--mfb-space-3);
5034
+ border: 1px solid var(--mfb-border);
5035
+ border-radius: var(--mfb-radius-lg);
5036
+ background: var(--mfb-surface);
5037
+ color: var(--mfb-text);
5038
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
5039
+ font-size: var(--mfb-text-sm);
5040
+ }
5041
+ .page-peek-title {
5042
+ font-weight: 700;
5043
+ margin-bottom: var(--mfb-space-2);
5044
+ color: var(--mfb-text-muted);
5045
+ text-transform: uppercase;
5046
+ font-size: var(--mfb-text-xs);
5047
+ letter-spacing: 0.04em;
5048
+ }
5049
+ .page-peek-row {
5050
+ display: flex;
5051
+ align-items: center;
5052
+ gap: var(--mfb-space-2);
5053
+ width: 100%;
5054
+ padding: var(--mfb-space-2);
5055
+ border: none;
5056
+ border-radius: var(--mfb-radius);
5057
+ background: none;
5058
+ color: var(--mfb-text);
5059
+ text-align: left;
5060
+ cursor: pointer;
5061
+ }
5062
+ .page-peek-row:hover {
5063
+ background: var(--mfb-surface-2);
5064
+ }
5065
+ .page-peek-dot {
5066
+ width: 8px;
5067
+ height: 8px;
5068
+ border-radius: 50%;
5069
+ flex: none;
5070
+ }
5071
+ .page-peek-dot.status-new { background: #1e40af; }
5072
+ .page-peek-dot.status-in_progress { background: #92400e; }
5073
+ .page-peek-dot.status-awaiting_validation { background: #6b21a8; }
5074
+ .page-peek-dot.status-closed { background: #065f46; }
5075
+ .page-peek-dot.status-rejected { background: #991b1b; }
5076
+ .page-peek-desc {
5077
+ overflow: hidden;
5078
+ white-space: nowrap;
5079
+ text-overflow: ellipsis;
5080
+ }
5081
+ .page-peek-footer {
5082
+ display: flex;
5083
+ justify-content: space-between;
5084
+ margin-top: var(--mfb-space-2);
5085
+ padding-top: var(--mfb-space-2);
5086
+ border-top: 1px solid var(--mfb-border);
5087
+ }
5088
+ .page-peek-viewall,
5089
+ .page-peek-send {
5090
+ border: none;
5091
+ background: none;
5092
+ padding: 0;
5093
+ cursor: pointer;
5094
+ color: var(--mfb-accent);
5095
+ font-weight: 600;
5096
+ font-size: var(--mfb-text-sm);
5097
+ }
5098
+ .page-peek-skeleton div {
5099
+ height: 14px;
5100
+ border-radius: 4px;
5101
+ background: var(--mfb-surface-2);
5102
+ margin: var(--mfb-space-2) 0;
5103
+ }
4701
5104
  `;
4702
5105
 
4703
5106
  // src/widget/mount.tsx
4704
- import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs12 } from "preact/jsx-runtime";
5107
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs14 } from "preact/jsx-runtime";
4705
5108
  function computeFabVisible(opts, externalId, visibilityAllowed) {
4706
5109
  if (!opts.showFAB) return false;
4707
5110
  if (opts.getExternalId !== void 0 && !externalId) return false;
@@ -4722,8 +5125,9 @@ function mountWidget(options) {
4722
5125
  render(h(Root, { state }), mountPoint);
4723
5126
  }
4724
5127
  function clearSelected(s) {
4725
- const { selectedReportId: _drop, ...rest } = s;
5128
+ const { selectedReportId: _drop, boardSelectId: _drop2, ...rest } = s;
4726
5129
  void _drop;
5130
+ void _drop2;
4727
5131
  return rest;
4728
5132
  }
4729
5133
  function openWidget(externalId) {
@@ -4738,10 +5142,11 @@ function mountWidget(options) {
4738
5142
  }
4739
5143
  }
4740
5144
  function Root({ state }) {
4741
- const handleSubmit = useCallback(async (values) => {
5145
+ const handleSubmit = useCallback2(async (values) => {
4742
5146
  rerender({ ...currentState, status: "submitting" });
4743
5147
  try {
4744
5148
  await options.onSubmit(values);
5149
+ pageSignal.refresh();
4745
5150
  rerender({ ...currentState, status: "success" });
4746
5151
  if (postSubmitTimer !== null) clearTimeout(postSubmitTimer);
4747
5152
  postSubmitTimer = setTimeout(() => {
@@ -4767,10 +5172,18 @@ function mountWidget(options) {
4767
5172
  }
4768
5173
  }, []);
4769
5174
  const externalId = options.getExternalId?.();
4770
- const [visibilityAllowed, setVisibilityAllowed] = useState7(
5175
+ const pageActivityEnabled = options.showPageActivity !== false;
5176
+ const pageSignal = usePageOpenCount({
5177
+ ...options.api !== void 0 && { api: options.api },
5178
+ ...externalId !== void 0 && { externalId },
5179
+ ...options.getCurrentPage !== void 0 && { getCurrentPage: options.getCurrentPage },
5180
+ enabled: pageActivityEnabled
5181
+ });
5182
+ const fabLabel = pageSignal.open === 1 ? options.strings["page.badge.aria.one"] : pageSignal.open > 0 ? options.strings["page.badge.aria"].replace("{n}", String(pageSignal.open)) : options.strings["fab.label"];
5183
+ const [visibilityAllowed, setVisibilityAllowed] = useState9(
4771
5184
  !options.requiresVisibilityCheck
4772
5185
  );
4773
- useEffect8(() => {
5186
+ useEffect10(() => {
4774
5187
  if (!options.requiresVisibilityCheck) {
4775
5188
  setVisibilityAllowed(true);
4776
5189
  return;
@@ -4791,23 +5204,74 @@ function mountWidget(options) {
4791
5204
  }, [externalId]);
4792
5205
  const fabVisible = computeFabVisible(options, externalId, visibilityAllowed);
4793
5206
  const showMineTab = Boolean(options.api && externalId);
4794
- return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4795
- fabVisible && /* @__PURE__ */ jsx12(
4796
- Fab,
5207
+ const [peekOpen, setPeekOpen] = useState9(false);
5208
+ const peekTimers = useRef7({});
5209
+ const peekEnter = () => {
5210
+ if (peekTimers.current.close) clearTimeout(peekTimers.current.close);
5211
+ peekTimers.current.open = setTimeout(() => setPeekOpen(true), 250);
5212
+ };
5213
+ const peekLeave = () => {
5214
+ if (peekTimers.current.open) clearTimeout(peekTimers.current.open);
5215
+ peekTimers.current.close = setTimeout(() => setPeekOpen(false), 140);
5216
+ };
5217
+ return /* @__PURE__ */ jsxs14(Fragment3, { children: [
5218
+ fabVisible && /* @__PURE__ */ jsxs14(
5219
+ "div",
4797
5220
  {
4798
- label: options.strings["fab.label"],
4799
- onClick: () => openWidget(externalId)
5221
+ class: "fab-area",
5222
+ onMouseEnter: peekEnter,
5223
+ onMouseLeave: peekLeave,
5224
+ onFocusIn: (e) => {
5225
+ if (peekTimers.current.close) clearTimeout(peekTimers.current.close);
5226
+ if (e.target.matches?.(":focus-visible")) setPeekOpen(true);
5227
+ },
5228
+ onFocusOut: () => setPeekOpen(false),
5229
+ onKeyDown: (e) => e.key === "Escape" && setPeekOpen(false),
5230
+ children: [
5231
+ peekOpen && !state.open && pageActivityEnabled && pageSignal.open > 0 && options.api && externalId && /* @__PURE__ */ jsx14(
5232
+ PagePeekPanel,
5233
+ {
5234
+ api: options.api,
5235
+ externalId,
5236
+ strings: options.strings,
5237
+ ...options.getCurrentPage !== void 0 && {
5238
+ getCurrentPage: options.getCurrentPage
5239
+ },
5240
+ open: pageSignal.open,
5241
+ onViewAll: () => {
5242
+ setPeekOpen(false);
5243
+ rerender({ ...currentState, open: true, tab: "board" });
5244
+ },
5245
+ onPickRow: (reportId) => {
5246
+ setPeekOpen(false);
5247
+ rerender({ ...currentState, open: true, tab: "board", boardSelectId: reportId });
5248
+ },
5249
+ onSend: () => {
5250
+ setPeekOpen(false);
5251
+ openWidget(externalId);
5252
+ }
5253
+ }
5254
+ ),
5255
+ /* @__PURE__ */ jsx14(
5256
+ Fab,
5257
+ {
5258
+ label: fabLabel,
5259
+ onClick: () => openWidget(externalId),
5260
+ ...pageSignal.open > 0 && { count: pageSignal.open }
5261
+ }
5262
+ )
5263
+ ]
4800
5264
  }
4801
5265
  ),
4802
- state.open && /* @__PURE__ */ jsxs12(
5266
+ state.open && /* @__PURE__ */ jsxs14(
4803
5267
  Modal,
4804
5268
  {
4805
5269
  onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
4806
5270
  closeLabel: options.strings["form.close"],
4807
5271
  expanded: state.tab === "board",
4808
5272
  children: [
4809
- showMineTab && /* @__PURE__ */ jsxs12("div", { class: "tab-strip", role: "tablist", children: [
4810
- /* @__PURE__ */ jsx12(
5273
+ showMineTab && /* @__PURE__ */ jsxs14("div", { class: "tab-strip", role: "tablist", children: [
5274
+ /* @__PURE__ */ jsx14(
4811
5275
  "button",
4812
5276
  {
4813
5277
  type: "button",
@@ -4818,7 +5282,7 @@ function mountWidget(options) {
4818
5282
  children: options.strings["tab.send"]
4819
5283
  }
4820
5284
  ),
4821
- /* @__PURE__ */ jsx12(
5285
+ /* @__PURE__ */ jsx14(
4822
5286
  "button",
4823
5287
  {
4824
5288
  type: "button",
@@ -4829,7 +5293,7 @@ function mountWidget(options) {
4829
5293
  children: options.strings["tab.mine"]
4830
5294
  }
4831
5295
  ),
4832
- /* @__PURE__ */ jsx12(
5296
+ /* @__PURE__ */ jsx14(
4833
5297
  "button",
4834
5298
  {
4835
5299
  type: "button",
@@ -4840,7 +5304,7 @@ function mountWidget(options) {
4840
5304
  children: options.strings["tab.changelog"]
4841
5305
  }
4842
5306
  ),
4843
- /* @__PURE__ */ jsx12(
5307
+ /* @__PURE__ */ jsx14(
4844
5308
  "button",
4845
5309
  {
4846
5310
  type: "button",
@@ -4852,17 +5316,27 @@ function mountWidget(options) {
4852
5316
  }
4853
5317
  )
4854
5318
  ] }),
4855
- state.tab === "send" && /* @__PURE__ */ jsx12(
4856
- Form,
4857
- {
4858
- strings: options.strings,
4859
- onSubmit: handleSubmit,
4860
- onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
4861
- status: state.status,
4862
- ...state.error !== void 0 && { errorMessage: state.error }
4863
- }
4864
- ),
4865
- state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
5319
+ state.tab === "send" && /* @__PURE__ */ jsxs14(Fragment3, { children: [
5320
+ showMineTab && pageActivityEnabled && /* @__PURE__ */ jsx14(
5321
+ PageActivityStrip,
5322
+ {
5323
+ open: pageSignal.open,
5324
+ strings: options.strings,
5325
+ onView: () => rerender(clearSelected({ ...currentState, tab: "board" }))
5326
+ }
5327
+ ),
5328
+ /* @__PURE__ */ jsx14(
5329
+ Form,
5330
+ {
5331
+ strings: options.strings,
5332
+ onSubmit: handleSubmit,
5333
+ onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
5334
+ status: state.status,
5335
+ ...state.error !== void 0 && { errorMessage: state.error }
5336
+ }
5337
+ )
5338
+ ] }),
5339
+ state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx14(
4866
5340
  MineList,
4867
5341
  {
4868
5342
  api: options.api,
@@ -4871,7 +5345,7 @@ function mountWidget(options) {
4871
5345
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
4872
5346
  }
4873
5347
  ),
4874
- (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx12(
5348
+ (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx14(
4875
5349
  ReportDetailView,
4876
5350
  {
4877
5351
  api: options.api,
@@ -4881,7 +5355,7 @@ function mountWidget(options) {
4881
5355
  onBack: () => rerender(clearSelected({ ...currentState }))
4882
5356
  }
4883
5357
  ),
4884
- state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
5358
+ state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx14(
4885
5359
  ChangelogList,
4886
5360
  {
4887
5361
  api: options.api,
@@ -4892,13 +5366,14 @@ function mountWidget(options) {
4892
5366
  ),
4893
5367
  state.tab === "board" && options.api && externalId && // BoardView owns its own master/detail navigation — no
4894
5368
  // selectedReportId routing through the modal-level state.
4895
- /* @__PURE__ */ jsx12(
5369
+ /* @__PURE__ */ jsx14(
4896
5370
  BoardView,
4897
5371
  {
4898
5372
  api: options.api,
4899
5373
  externalId,
4900
5374
  strings: options.strings,
4901
- ...options.getCurrentPage !== void 0 && { getCurrentPage: options.getCurrentPage }
5375
+ ...options.getCurrentPage !== void 0 && { getCurrentPage: options.getCurrentPage },
5376
+ ...state.boardSelectId !== void 0 && { initialSelectedId: state.boardSelectId }
4902
5377
  }
4903
5378
  )
4904
5379
  ]
@@ -4993,8 +5468,8 @@ function createFeedback(config) {
4993
5468
  capture_method: captureMethod,
4994
5469
  technical_context
4995
5470
  };
4996
- if ("0.26.0") {
4997
- payload.widget_version = "0.26.0";
5471
+ if ("0.27.0") {
5472
+ payload.widget_version = "0.27.0";
4998
5473
  }
4999
5474
  if (manualScreenshots?.length) {
5000
5475
  payload.screenshots = manualScreenshots;
@@ -5050,6 +5525,9 @@ function createFeedback(config) {
5050
5525
  // Opt-in: FAB opens to the page-scoped Board (default off → Send-first).
5051
5526
  ...config.openToCurrentPageFeedback !== void 0 && {
5052
5527
  openToCurrentPageFeedback: config.openToCurrentPageFeedback
5528
+ },
5529
+ ...config.showPageActivity !== void 0 && {
5530
+ showPageActivity: config.showPageActivity
5053
5531
  }
5054
5532
  });
5055
5533
  let qaHandle;
@@ -5118,4 +5596,4 @@ function createFeedback(config) {
5118
5596
  export {
5119
5597
  createFeedback
5120
5598
  };
5121
- //# sourceMappingURL=chunk-FYF5KICC.mjs.map
5599
+ //# sourceMappingURL=chunk-BXUX5UTI.mjs.map