@mhosaic/feedback 0.26.1 → 0.27.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.
@@ -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) {
@@ -1418,7 +1444,13 @@ var STATUSES = [
1418
1444
  ];
1419
1445
  var TYPES = ["bug", "feature", "question", "praise", "typo"];
1420
1446
  var SEVERITIES = ["blocker", "high", "medium", "low"];
1421
- function BoardView({ api, externalId, strings, getCurrentPage }) {
1447
+ function BoardView({
1448
+ api,
1449
+ externalId,
1450
+ strings,
1451
+ getCurrentPage,
1452
+ initialSelectedId
1453
+ }) {
1422
1454
  const [filters, setFilters] = useState2(() => loadBoardView(externalId));
1423
1455
  const [thisPage, setThisPage] = useState2(true);
1424
1456
  useEffect2(() => {
@@ -1428,7 +1460,7 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1428
1460
  const [kpis, setKpis] = useState2(null);
1429
1461
  const [loading, setLoading] = useState2(true);
1430
1462
  const [error, setError] = useState2(null);
1431
- const [selectedId, setSelectedId] = useState2(null);
1463
+ const [selectedId, setSelectedId] = useState2(initialSelectedId ?? null);
1432
1464
  const [detailKey, setDetailKey] = useState2(0);
1433
1465
  const [reloadTick, setReloadTick] = useState2(0);
1434
1466
  const activeFilterCount = useMemo(() => {
@@ -1479,7 +1511,7 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1479
1511
  setDetailKey((k) => k + 1);
1480
1512
  };
1481
1513
  return /* @__PURE__ */ jsxs3("div", { class: "board-view", children: [
1482
- /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis }),
1514
+ /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis, thisPage }),
1483
1515
  /* @__PURE__ */ jsx3(
1484
1516
  BoardFilters,
1485
1517
  {
@@ -1510,7 +1542,14 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1510
1542
  )
1511
1543
  ] }),
1512
1544
  !error && loading && !page && /* @__PURE__ */ jsx3(BoardListSkeleton, {}),
1513
- !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
+ ),
1514
1553
  !error && page && page.results.length > 0 && /* @__PURE__ */ jsx3(
1515
1554
  BoardList,
1516
1555
  {
@@ -1518,7 +1557,8 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1518
1557
  selectedId,
1519
1558
  onPick: onPickRow,
1520
1559
  strings,
1521
- total: page.count
1560
+ total: page.count,
1561
+ thisPage
1522
1562
  }
1523
1563
  )
1524
1564
  ] }),
@@ -1543,9 +1583,10 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1543
1583
  }
1544
1584
  function BoardHeader({
1545
1585
  strings,
1546
- kpis
1586
+ kpis,
1587
+ thisPage
1547
1588
  }) {
1548
- 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"];
1549
1590
  return /* @__PURE__ */ jsxs3("header", { class: "board-header", children: [
1550
1591
  /* @__PURE__ */ jsxs3("div", { class: "board-header-title", children: [
1551
1592
  /* @__PURE__ */ jsx3("span", { class: "board-header-emoji", "aria-hidden": "true", children: "\u{1F4CB}" }),
@@ -1640,6 +1681,28 @@ function BoardFilters({
1640
1681
  };
1641
1682
  const clear = () => onChange({});
1642
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
+ ] }),
1643
1706
  /* @__PURE__ */ jsx3(
1644
1707
  "select",
1645
1708
  {
@@ -1710,19 +1773,6 @@ function BoardFilters({
1710
1773
  ),
1711
1774
  /* @__PURE__ */ jsx3("span", { children: strings["board.filter.mine"] })
1712
1775
  ] }),
1713
- /* @__PURE__ */ jsxs3(
1714
- "button",
1715
- {
1716
- type: "button",
1717
- class: `board-chip ${thisPage ? "board-chip--on" : ""}`,
1718
- onClick: onToggleThisPage,
1719
- children: [
1720
- "\u{1F4CD} ",
1721
- strings["board.scope.thisPage"],
1722
- thisPage ? " \u2715" : ""
1723
- ]
1724
- }
1725
- ),
1726
1776
  activeCount > 0 && /* @__PURE__ */ jsxs3("button", { type: "button", class: "board-filter-clear", onClick: clear, children: [
1727
1777
  /* @__PURE__ */ jsx3(CloseIcon, {}),
1728
1778
  strings["board.filter.clear"]
@@ -1734,10 +1784,11 @@ function BoardList({
1734
1784
  selectedId,
1735
1785
  onPick,
1736
1786
  strings,
1737
- total
1787
+ total,
1788
+ thisPage
1738
1789
  }) {
1739
1790
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1740
- /* @__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)) }),
1741
1792
  /* @__PURE__ */ jsx3("ul", { class: "board-list", role: "list", children: rows.map((row) => /* @__PURE__ */ jsx3(
1742
1793
  BoardRowCard,
1743
1794
  {
@@ -1783,11 +1834,16 @@ function BoardRowCard({
1783
1834
  }
1784
1835
  ) });
1785
1836
  }
1786
- function BoardEmpty({ strings }) {
1837
+ function BoardEmpty({
1838
+ strings,
1839
+ thisPage,
1840
+ onAllPages
1841
+ }) {
1787
1842
  return /* @__PURE__ */ jsxs3("div", { class: "board-empty", children: [
1788
1843
  /* @__PURE__ */ jsx3(EmptyIllustration, {}),
1789
- /* @__PURE__ */ jsx3("h3", { children: strings["board.list.empty.title"] }),
1790
- /* @__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"] })
1791
1847
  ] });
1792
1848
  }
1793
1849
  function BoardListSkeleton() {
@@ -2030,8 +2086,11 @@ function BugIcon() {
2030
2086
  }
2031
2087
  );
2032
2088
  }
2033
- function Fab({ label, onClick }) {
2034
- 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
+ ] });
2035
2094
  }
2036
2095
 
2037
2096
  // src/widget/Form.tsx
@@ -3037,6 +3096,136 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3037
3096
  );
3038
3097
  }
3039
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
+
3040
3229
  // src/widget/styles.ts
3041
3230
  var WIDGET_STYLES = `
3042
3231
  :host {
@@ -3111,10 +3300,20 @@ var WIDGET_STYLES = `
3111
3300
  soft-black background with a light steel-blue lucide-bug glyph;
3112
3301
  custom SVG inlined (no emoji \u2014 emoji renders inconsistently across
3113
3302
  OSes and can't inherit color). Two-layer elevation, scale-on-press. */
3114
- .fab {
3303
+ .fab-area {
3115
3304
  position: fixed;
3116
3305
  bottom: 24px;
3117
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 {
3118
3317
  width: 48px;
3119
3318
  height: 48px;
3120
3319
  border-radius: 999px;
@@ -3158,9 +3357,43 @@ var WIDGET_STYLES = `
3158
3357
  0 4px 12px rgba(0, 0, 0, 0.32),
3159
3358
  0 2px 4px rgba(0, 0, 0, 0.40); }
3160
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
+
3161
3391
  @media (prefers-reduced-motion: reduce) {
3162
3392
  .fab { transition: none; }
3163
3393
  .fab:hover, .fab:active { transform: none; }
3394
+ .fab-badge {
3395
+ animation: none;
3396
+ }
3164
3397
  }
3165
3398
 
3166
3399
  /* Backdrop \u2014 fade in with a slight blur for depth. The blur gives the
@@ -4507,6 +4740,59 @@ var WIDGET_STYLES = `
4507
4740
  background: var(--mfb-surface);
4508
4741
  }
4509
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
+
4510
4796
  /* Master/detail layout \u2014 two-column at >=900px, stacked below. */
4511
4797
  .board-body {
4512
4798
  display: grid;
@@ -4735,10 +5021,90 @@ var WIDGET_STYLES = `
4735
5021
  @media (prefers-reduced-motion: reduce) {
4736
5022
  .skeleton-line { animation: none; }
4737
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
+ }
4738
5104
  `;
4739
5105
 
4740
5106
  // src/widget/mount.tsx
4741
- 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";
4742
5108
  function computeFabVisible(opts, externalId, visibilityAllowed) {
4743
5109
  if (!opts.showFAB) return false;
4744
5110
  if (opts.getExternalId !== void 0 && !externalId) return false;
@@ -4759,8 +5125,9 @@ function mountWidget(options) {
4759
5125
  render(h(Root, { state }), mountPoint);
4760
5126
  }
4761
5127
  function clearSelected(s) {
4762
- const { selectedReportId: _drop, ...rest } = s;
5128
+ const { selectedReportId: _drop, boardSelectId: _drop2, ...rest } = s;
4763
5129
  void _drop;
5130
+ void _drop2;
4764
5131
  return rest;
4765
5132
  }
4766
5133
  function openWidget(externalId) {
@@ -4775,10 +5142,11 @@ function mountWidget(options) {
4775
5142
  }
4776
5143
  }
4777
5144
  function Root({ state }) {
4778
- const handleSubmit = useCallback(async (values) => {
5145
+ const handleSubmit = useCallback2(async (values) => {
4779
5146
  rerender({ ...currentState, status: "submitting" });
4780
5147
  try {
4781
5148
  await options.onSubmit(values);
5149
+ pageSignal.refresh();
4782
5150
  rerender({ ...currentState, status: "success" });
4783
5151
  if (postSubmitTimer !== null) clearTimeout(postSubmitTimer);
4784
5152
  postSubmitTimer = setTimeout(() => {
@@ -4804,10 +5172,18 @@ function mountWidget(options) {
4804
5172
  }
4805
5173
  }, []);
4806
5174
  const externalId = options.getExternalId?.();
4807
- 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(
4808
5184
  !options.requiresVisibilityCheck
4809
5185
  );
4810
- useEffect8(() => {
5186
+ useEffect10(() => {
4811
5187
  if (!options.requiresVisibilityCheck) {
4812
5188
  setVisibilityAllowed(true);
4813
5189
  return;
@@ -4828,23 +5204,74 @@ function mountWidget(options) {
4828
5204
  }, [externalId]);
4829
5205
  const fabVisible = computeFabVisible(options, externalId, visibilityAllowed);
4830
5206
  const showMineTab = Boolean(options.api && externalId);
4831
- return /* @__PURE__ */ jsxs12(Fragment3, { children: [
4832
- fabVisible && /* @__PURE__ */ jsx12(
4833
- 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",
4834
5220
  {
4835
- label: options.strings["fab.label"],
4836
- 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
+ ]
4837
5264
  }
4838
5265
  ),
4839
- state.open && /* @__PURE__ */ jsxs12(
5266
+ state.open && /* @__PURE__ */ jsxs14(
4840
5267
  Modal,
4841
5268
  {
4842
5269
  onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
4843
5270
  closeLabel: options.strings["form.close"],
4844
5271
  expanded: state.tab === "board",
4845
5272
  children: [
4846
- showMineTab && /* @__PURE__ */ jsxs12("div", { class: "tab-strip", role: "tablist", children: [
4847
- /* @__PURE__ */ jsx12(
5273
+ showMineTab && /* @__PURE__ */ jsxs14("div", { class: "tab-strip", role: "tablist", children: [
5274
+ /* @__PURE__ */ jsx14(
4848
5275
  "button",
4849
5276
  {
4850
5277
  type: "button",
@@ -4855,7 +5282,7 @@ function mountWidget(options) {
4855
5282
  children: options.strings["tab.send"]
4856
5283
  }
4857
5284
  ),
4858
- /* @__PURE__ */ jsx12(
5285
+ /* @__PURE__ */ jsx14(
4859
5286
  "button",
4860
5287
  {
4861
5288
  type: "button",
@@ -4866,7 +5293,7 @@ function mountWidget(options) {
4866
5293
  children: options.strings["tab.mine"]
4867
5294
  }
4868
5295
  ),
4869
- /* @__PURE__ */ jsx12(
5296
+ /* @__PURE__ */ jsx14(
4870
5297
  "button",
4871
5298
  {
4872
5299
  type: "button",
@@ -4877,7 +5304,7 @@ function mountWidget(options) {
4877
5304
  children: options.strings["tab.changelog"]
4878
5305
  }
4879
5306
  ),
4880
- /* @__PURE__ */ jsx12(
5307
+ /* @__PURE__ */ jsx14(
4881
5308
  "button",
4882
5309
  {
4883
5310
  type: "button",
@@ -4889,17 +5316,27 @@ function mountWidget(options) {
4889
5316
  }
4890
5317
  )
4891
5318
  ] }),
4892
- state.tab === "send" && /* @__PURE__ */ jsx12(
4893
- Form,
4894
- {
4895
- strings: options.strings,
4896
- onSubmit: handleSubmit,
4897
- onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
4898
- status: state.status,
4899
- ...state.error !== void 0 && { errorMessage: state.error }
4900
- }
4901
- ),
4902
- 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(
4903
5340
  MineList,
4904
5341
  {
4905
5342
  api: options.api,
@@ -4908,7 +5345,7 @@ function mountWidget(options) {
4908
5345
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
4909
5346
  }
4910
5347
  ),
4911
- (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(
4912
5349
  ReportDetailView,
4913
5350
  {
4914
5351
  api: options.api,
@@ -4918,7 +5355,7 @@ function mountWidget(options) {
4918
5355
  onBack: () => rerender(clearSelected({ ...currentState }))
4919
5356
  }
4920
5357
  ),
4921
- state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
5358
+ state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx14(
4922
5359
  ChangelogList,
4923
5360
  {
4924
5361
  api: options.api,
@@ -4929,13 +5366,14 @@ function mountWidget(options) {
4929
5366
  ),
4930
5367
  state.tab === "board" && options.api && externalId && // BoardView owns its own master/detail navigation — no
4931
5368
  // selectedReportId routing through the modal-level state.
4932
- /* @__PURE__ */ jsx12(
5369
+ /* @__PURE__ */ jsx14(
4933
5370
  BoardView,
4934
5371
  {
4935
5372
  api: options.api,
4936
5373
  externalId,
4937
5374
  strings: options.strings,
4938
- ...options.getCurrentPage !== void 0 && { getCurrentPage: options.getCurrentPage }
5375
+ ...options.getCurrentPage !== void 0 && { getCurrentPage: options.getCurrentPage },
5376
+ ...state.boardSelectId !== void 0 && { initialSelectedId: state.boardSelectId }
4939
5377
  }
4940
5378
  )
4941
5379
  ]
@@ -5030,8 +5468,8 @@ function createFeedback(config) {
5030
5468
  capture_method: captureMethod,
5031
5469
  technical_context
5032
5470
  };
5033
- if ("0.26.1") {
5034
- payload.widget_version = "0.26.1";
5471
+ if ("0.27.1") {
5472
+ payload.widget_version = "0.27.1";
5035
5473
  }
5036
5474
  if (manualScreenshots?.length) {
5037
5475
  payload.screenshots = manualScreenshots;
@@ -5087,6 +5525,9 @@ function createFeedback(config) {
5087
5525
  // Opt-in: FAB opens to the page-scoped Board (default off → Send-first).
5088
5526
  ...config.openToCurrentPageFeedback !== void 0 && {
5089
5527
  openToCurrentPageFeedback: config.openToCurrentPageFeedback
5528
+ },
5529
+ ...config.showPageActivity !== void 0 && {
5530
+ showPageActivity: config.showPageActivity
5090
5531
  }
5091
5532
  });
5092
5533
  let qaHandle;
@@ -5155,4 +5596,4 @@ function createFeedback(config) {
5155
5596
  export {
5156
5597
  createFeedback
5157
5598
  };
5158
- //# sourceMappingURL=chunk-3CYS47XC.mjs.map
5599
+ //# sourceMappingURL=chunk-ZFXGV5W5.mjs.map