spaps-issue-reporting-react 0.1.1 → 0.1.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.1] - 2026-04-05
4
+
5
+ - Docs: align package README metadata with published version.
6
+
3
7
  ## [0.1.0] - 2026-03-29
4
8
 
5
9
  - Added the first shared React issue-reporting package for SPAPS consumers.
package/README.md CHANGED
@@ -17,9 +17,9 @@ This package targets `Node.js >=18` and React 18+.
17
17
  | Need | Package gives you |
18
18
  | --- | --- |
19
19
  | A visible issue-report entry point | Floating button with open/recent state |
20
- | A guided reporting flow | Report mode, highlighted sections, create/edit/reply modal |
20
+ | A guided reporting flow | Page-first create flow, optional section picking, create/edit/reply modal |
21
21
  | A lightweight integration contract | Any client that exposes `issueReporting.*` methods |
22
- | App-level control | Eligibility, reporter identity, scope, and copy stay in your app |
22
+ | App-level control | Eligibility, reporter identity, scope, page policy, and copy stay in your app |
23
23
 
24
24
  ## Quick Start
25
25
 
@@ -27,6 +27,7 @@ This package targets `Node.js >=18` and React 18+.
27
27
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
28
28
  import {
29
29
  FloatingIssueReportButton,
30
+ IssueReportingPageConfig,
30
31
  IssueReportingProvider,
31
32
  ReportableSection,
32
33
  } from "spaps-issue-reporting-react";
@@ -45,6 +46,8 @@ export function AppShell() {
45
46
  allowTenantScope
46
47
  defaultScope="mine"
47
48
  >
49
+ <IssueReportingPageConfig createMode="surface_preferred" />
50
+
48
51
  <ReportableSection
49
52
  reportableName={{
50
53
  componentKey: "patient_chart",
@@ -66,20 +69,26 @@ export function AppShell() {
66
69
  ## What Your App Still Owns
67
70
 
68
71
  - A client with `issueReporting.getStatus`, `list`, `get`, `create`, `update`, and `reply`.
72
+ - Any auth and token refresh behavior needed by that client.
69
73
  - Eligibility rules such as feature flags, account state, or role checks.
70
74
  - The current principal ID and optional role hint passed into the provider.
71
75
  - Whether users can switch between `mine` and `tenant` queue scope.
76
+ - Whether a page defaults to `general_page`, `surface_required`, or `surface_preferred` reporting.
72
77
  - Styling integration if your build strips package utility classes.
73
78
  - Any app-specific copy overrides.
79
+ - Origin registration on the owning SPAPS application if the browser calls SPAPS directly with a publishable key.
80
+
81
+ For direct browser integrations, `allowed_origins` is stored on the SPAPS `applications` row that owns the publishable key. It is not configured on this package. If multiple hostnames share one SPAPS application, put all of them on that row. Updating `allowed_origins` does not require restarting SPAPS.
74
82
 
75
83
  ## Exported Surface
76
84
 
77
85
  | Export | Purpose |
78
86
  | --- | --- |
79
87
  | `IssueReportingProvider` | Owns queries, modal state, copy, scope, and report-mode behavior |
88
+ | `IssueReportingPageConfig` | Per-page override for `general_page`, `surface_required`, or `surface_preferred` create policy |
80
89
  | `FloatingIssueReportButton` | Renders the floating entry point, popover, and modal |
81
90
  | `ReportableSection` | Marks a region as selectable when report mode is active |
82
- | `useIssueReporting` | Full provider state, including `enterReportMode()` |
91
+ | `useIssueReporting` | Full provider state, including `startNewIssue()`, `openPageIssueModal()`, and `enterReportMode()` |
83
92
  | `useIssueReportingStatus` | Query helper for summary state |
84
93
  | `useIssueReportingHistory` | Query helper for filtered history lists |
85
94
  | `useIssueReportingMutations` | Mutation helpers for create, update, and reply flows |
@@ -107,10 +116,18 @@ npm test
107
116
 
108
117
  Make sure `FloatingIssueReportButton` is rendered inside `IssueReportingProvider` and that `isEligible` is `true`.
109
118
 
119
+ ### I want page-level reporting without wrapping every widget
120
+
121
+ That is the default. `Report This Page` opens immediately and captures the current route plus the inventory of visible registered reportable sections on the active page.
122
+
110
123
  ### Report mode turns on, but sections do not respond
111
124
 
112
125
  Wrap the target UI in `ReportableSection`, or call `useIssueReporting().selectPanel(...)` from a custom control.
113
126
 
127
+ ### This page should force a specific surface selection
128
+
129
+ Render `IssueReportingPageConfig` with `createMode="surface_required"` inside that page's visible provider subtree. Hidden mounted routes, tabs, or panels do not affect the active page's policy.
130
+
114
131
  ### Tenant scope never appears
115
132
 
116
133
  Set `allowTenantScope={true}`. The provider defaults to `mine`.
@@ -135,6 +152,10 @@ No. You only need a client object that implements the `issueReporting` methods e
135
152
 
136
153
  Yes. Use `useIssueReporting().enterReportMode()` inside the provider tree.
137
154
 
155
+ ### Can I open a page-level report from my own button?
156
+
157
+ Yes. Use `useIssueReporting().openPageIssueModal()` or `useIssueReporting().startNewIssue()`.
158
+
138
159
  ### Can I report plain strings instead of structured descriptors?
139
160
 
140
161
  Yes. `reportableName` accepts either a string or a `{ componentKey, componentLabel, ... }` object.
@@ -147,6 +168,17 @@ Yes. Drive that from `isEligible`.
147
168
 
148
169
  No. It only renders scope choices that your app explicitly allows.
149
170
 
171
+ ### Does this package manage CORS or `allowed_origins`?
172
+
173
+ No. This package is UI only. If your browser app calls SPAPS directly with a publishable key, the relevant SPAPS application row must include that browser origin in `allowed_origins`.
174
+
175
+ ## Metadata
176
+
177
+ - `package_name`: `spaps-issue-reporting-react`
178
+ - `latest_version`: `0.1.1`
179
+ - `minimum_runtime`: `Node.js >=18.0.0`
180
+ - `api_base_url`: `https://api.sweetpotato.dev`
181
+
150
182
  ## About Contributions
151
183
 
152
184
  > *About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
package/dist/index.d.mts CHANGED
@@ -31,11 +31,14 @@ interface ReportableTargetDescriptor {
31
31
  metadata?: Record<string, unknown>;
32
32
  }
33
33
  type ReportableInput = string | ReportableTargetDescriptor;
34
+ type IssueReportingCreateMode = "general_page" | "surface_required" | "surface_preferred";
34
35
  type IssueHistoryFilter = "all" | "unresolved" | "resolved";
35
36
  interface IssueReportingCopy {
36
37
  entryAriaLabel: string;
37
38
  popoverTitle: string;
38
39
  reportNewAction: string;
40
+ reportPageAction: string;
41
+ reportSpecificAction: string;
39
42
  filtersAll: string;
40
43
  filtersUnresolved: string;
41
44
  filtersResolved: string;
@@ -52,6 +55,9 @@ interface IssueReportingCopy {
52
55
  reportModeTitle: string;
53
56
  reportModeDescription: string;
54
57
  reportModeCancelAction: string;
58
+ generalPageTargetLabel: string;
59
+ surfaceRequiredDescription: string;
60
+ specificSectionUnavailableDescription: string;
55
61
  createTitlePrefix: string;
56
62
  editTitlePrefix: string;
57
63
  replyTitlePrefix: string;
@@ -82,6 +88,7 @@ interface IssueReportingProviderProps {
82
88
  getPageUrl?: () => string;
83
89
  defaultScope?: IssueReportScope;
84
90
  allowTenantScope?: boolean;
91
+ defaultCreateMode?: IssueReportingCreateMode;
85
92
  copy?: Partial<IssueReportingCopy>;
86
93
  children: ReactNode;
87
94
  }
@@ -89,6 +96,9 @@ interface FloatingIssueReportButtonProps {
89
96
  className?: string;
90
97
  positionClassName?: string;
91
98
  }
99
+ interface IssueReportingPageConfigProps {
100
+ createMode: IssueReportingCreateMode;
101
+ }
92
102
 
93
103
  declare function FloatingIssueReportButton({ className, positionClassName, }: FloatingIssueReportButtonProps): react_jsx_runtime.JSX.Element | null;
94
104
  declare function ReportableSection({ reportableName, children, className, as: Component, }: {
@@ -96,7 +106,7 @@ declare function ReportableSection({ reportableName, children, className, as: Co
96
106
  children: React__default.ReactNode;
97
107
  className?: string;
98
108
  as?: keyof JSX.IntrinsicElements;
99
- }): react_jsx_runtime.JSX.Element;
109
+ }): React__default.ReactElement<any, string | React__default.JSXElementConstructor<any>>;
100
110
 
101
111
  type ModalMode = "create" | "edit" | "reply";
102
112
  type ResolvedTarget = {
@@ -122,9 +132,12 @@ type IssueReportingContextValue = {
122
132
  principalId: string | null;
123
133
  copy: IssueReportingCopy;
124
134
  isReportMode: boolean;
135
+ hasRegisteredTargets: boolean;
125
136
  enterReportMode: () => void;
126
137
  cancelReportMode: () => void;
127
138
  selectPanel: (target: ReportableInput) => void;
139
+ openPageIssueModal: () => void;
140
+ startNewIssue: () => void;
128
141
  isPopoverOpen: boolean;
129
142
  openPopover: () => void;
130
143
  closePopover: () => void;
@@ -135,6 +148,7 @@ type IssueReportingContextValue = {
135
148
  scope: IssueReportScope;
136
149
  setScope: (scope: IssueReportScope) => void;
137
150
  allowTenantScope: boolean;
151
+ createMode: IssueReportingCreateMode;
138
152
  };
139
153
  declare const defaultIssueReportingCopy: IssueReportingCopy;
140
154
  declare const issueReportingKeys: {
@@ -155,6 +169,7 @@ declare function getEntryPointState(status?: {
155
169
  declare function getEntryPointClassName(state: "open" | "recent_resolved" | "neutral"): string;
156
170
  declare function getIssueNoteLengthMessage(note: string, copy: IssueReportingCopy): string;
157
171
  declare function useIssueReporting(): IssueReportingContextValue;
172
+ declare function IssueReportingPageConfig({ createMode, }: IssueReportingPageConfigProps): react_jsx_runtime.JSX.Element;
158
173
  declare function useIssueReportingStatus(): _tanstack_react_query.UseQueryResult<spaps_types.IssueReportStatusResult, Error>;
159
174
  declare function useIssueReportingHistory(filter?: IssueHistoryFilter): {
160
175
  items: IssueReport[];
@@ -347,14 +362,17 @@ declare function useIssueReportingMutations(): {
347
362
  reporterRoleHint?: string;
348
363
  }, unknown>;
349
364
  };
350
- declare function IssueReportingProvider({ client, isEligible, reporterRoleHint, principalId, getPageUrl, defaultScope, allowTenantScope, copy, children, }: IssueReportingProviderProps): react_jsx_runtime.JSX.Element;
365
+ declare function IssueReportingProvider({ client, isEligible, reporterRoleHint, principalId, getPageUrl, defaultScope, allowTenantScope, defaultCreateMode, copy, children, }: IssueReportingProviderProps): react_jsx_runtime.JSX.Element;
351
366
 
352
367
  interface ReportModeContextValue {
353
368
  isReportMode: boolean;
354
369
  selectPanel: (target: ReportableInput) => void;
355
370
  cancelReportMode: () => void;
371
+ hasRegisteredTargets: boolean;
372
+ registerTarget: (id: symbol, target: ReportableInput, getElement: () => Element | null) => void;
373
+ unregisterTarget: (id: symbol) => void;
356
374
  }
357
375
  declare const ReportModeContext: React.Context<ReportModeContextValue | null>;
358
376
  declare function useReportMode(): ReportModeContextValue | null;
359
377
 
360
- export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, type IssueReportingClient, type IssueReportingCopy, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, issueReportingKeys, useIssueReporting, useIssueReportingHistory, useIssueReportingMutations, useIssueReportingStatus, useReportMode };
378
+ export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, type IssueReportingClient, type IssueReportingCopy, type IssueReportingCreateMode, IssueReportingPageConfig, type IssueReportingPageConfigProps, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, issueReportingKeys, useIssueReporting, useIssueReportingHistory, useIssueReportingMutations, useIssueReportingStatus, useReportMode };
package/dist/index.d.ts CHANGED
@@ -31,11 +31,14 @@ interface ReportableTargetDescriptor {
31
31
  metadata?: Record<string, unknown>;
32
32
  }
33
33
  type ReportableInput = string | ReportableTargetDescriptor;
34
+ type IssueReportingCreateMode = "general_page" | "surface_required" | "surface_preferred";
34
35
  type IssueHistoryFilter = "all" | "unresolved" | "resolved";
35
36
  interface IssueReportingCopy {
36
37
  entryAriaLabel: string;
37
38
  popoverTitle: string;
38
39
  reportNewAction: string;
40
+ reportPageAction: string;
41
+ reportSpecificAction: string;
39
42
  filtersAll: string;
40
43
  filtersUnresolved: string;
41
44
  filtersResolved: string;
@@ -52,6 +55,9 @@ interface IssueReportingCopy {
52
55
  reportModeTitle: string;
53
56
  reportModeDescription: string;
54
57
  reportModeCancelAction: string;
58
+ generalPageTargetLabel: string;
59
+ surfaceRequiredDescription: string;
60
+ specificSectionUnavailableDescription: string;
55
61
  createTitlePrefix: string;
56
62
  editTitlePrefix: string;
57
63
  replyTitlePrefix: string;
@@ -82,6 +88,7 @@ interface IssueReportingProviderProps {
82
88
  getPageUrl?: () => string;
83
89
  defaultScope?: IssueReportScope;
84
90
  allowTenantScope?: boolean;
91
+ defaultCreateMode?: IssueReportingCreateMode;
85
92
  copy?: Partial<IssueReportingCopy>;
86
93
  children: ReactNode;
87
94
  }
@@ -89,6 +96,9 @@ interface FloatingIssueReportButtonProps {
89
96
  className?: string;
90
97
  positionClassName?: string;
91
98
  }
99
+ interface IssueReportingPageConfigProps {
100
+ createMode: IssueReportingCreateMode;
101
+ }
92
102
 
93
103
  declare function FloatingIssueReportButton({ className, positionClassName, }: FloatingIssueReportButtonProps): react_jsx_runtime.JSX.Element | null;
94
104
  declare function ReportableSection({ reportableName, children, className, as: Component, }: {
@@ -96,7 +106,7 @@ declare function ReportableSection({ reportableName, children, className, as: Co
96
106
  children: React__default.ReactNode;
97
107
  className?: string;
98
108
  as?: keyof JSX.IntrinsicElements;
99
- }): react_jsx_runtime.JSX.Element;
109
+ }): React__default.ReactElement<any, string | React__default.JSXElementConstructor<any>>;
100
110
 
101
111
  type ModalMode = "create" | "edit" | "reply";
102
112
  type ResolvedTarget = {
@@ -122,9 +132,12 @@ type IssueReportingContextValue = {
122
132
  principalId: string | null;
123
133
  copy: IssueReportingCopy;
124
134
  isReportMode: boolean;
135
+ hasRegisteredTargets: boolean;
125
136
  enterReportMode: () => void;
126
137
  cancelReportMode: () => void;
127
138
  selectPanel: (target: ReportableInput) => void;
139
+ openPageIssueModal: () => void;
140
+ startNewIssue: () => void;
128
141
  isPopoverOpen: boolean;
129
142
  openPopover: () => void;
130
143
  closePopover: () => void;
@@ -135,6 +148,7 @@ type IssueReportingContextValue = {
135
148
  scope: IssueReportScope;
136
149
  setScope: (scope: IssueReportScope) => void;
137
150
  allowTenantScope: boolean;
151
+ createMode: IssueReportingCreateMode;
138
152
  };
139
153
  declare const defaultIssueReportingCopy: IssueReportingCopy;
140
154
  declare const issueReportingKeys: {
@@ -155,6 +169,7 @@ declare function getEntryPointState(status?: {
155
169
  declare function getEntryPointClassName(state: "open" | "recent_resolved" | "neutral"): string;
156
170
  declare function getIssueNoteLengthMessage(note: string, copy: IssueReportingCopy): string;
157
171
  declare function useIssueReporting(): IssueReportingContextValue;
172
+ declare function IssueReportingPageConfig({ createMode, }: IssueReportingPageConfigProps): react_jsx_runtime.JSX.Element;
158
173
  declare function useIssueReportingStatus(): _tanstack_react_query.UseQueryResult<spaps_types.IssueReportStatusResult, Error>;
159
174
  declare function useIssueReportingHistory(filter?: IssueHistoryFilter): {
160
175
  items: IssueReport[];
@@ -347,14 +362,17 @@ declare function useIssueReportingMutations(): {
347
362
  reporterRoleHint?: string;
348
363
  }, unknown>;
349
364
  };
350
- declare function IssueReportingProvider({ client, isEligible, reporterRoleHint, principalId, getPageUrl, defaultScope, allowTenantScope, copy, children, }: IssueReportingProviderProps): react_jsx_runtime.JSX.Element;
365
+ declare function IssueReportingProvider({ client, isEligible, reporterRoleHint, principalId, getPageUrl, defaultScope, allowTenantScope, defaultCreateMode, copy, children, }: IssueReportingProviderProps): react_jsx_runtime.JSX.Element;
351
366
 
352
367
  interface ReportModeContextValue {
353
368
  isReportMode: boolean;
354
369
  selectPanel: (target: ReportableInput) => void;
355
370
  cancelReportMode: () => void;
371
+ hasRegisteredTargets: boolean;
372
+ registerTarget: (id: symbol, target: ReportableInput, getElement: () => Element | null) => void;
373
+ unregisterTarget: (id: symbol) => void;
356
374
  }
357
375
  declare const ReportModeContext: React.Context<ReportModeContextValue | null>;
358
376
  declare function useReportMode(): ReportModeContextValue | null;
359
377
 
360
- export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, type IssueReportingClient, type IssueReportingCopy, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, issueReportingKeys, useIssueReporting, useIssueReportingHistory, useIssueReportingMutations, useIssueReportingStatus, useReportMode };
378
+ export { FloatingIssueReportButton, type FloatingIssueReportButtonProps, type IssueHistoryFilter, type IssueReportingClient, type IssueReportingCopy, type IssueReportingCreateMode, IssueReportingPageConfig, type IssueReportingPageConfigProps, IssueReportingProvider, type IssueReportingProviderProps, ReportModeContext, type ReportModeContextValue, type ReportableInput, ReportableSection, type ReportableTargetDescriptor, defaultIssueReportingCopy, filterIssueReports, getEntryPointClassName, getEntryPointState, getIssueNoteLengthMessage, getIssueStatusBadgeLabel, getIssueStatusClassName, isClosedIssueStatus, isOpenIssueStatus, issueReportingKeys, useIssueReporting, useIssueReportingHistory, useIssueReportingMutations, useIssueReportingStatus, useReportMode };
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  FloatingIssueReportButton: () => FloatingIssueReportButton,
34
+ IssueReportingPageConfig: () => IssueReportingPageConfig,
34
35
  IssueReportingProvider: () => IssueReportingProvider,
35
36
  ReportModeContext: () => ReportModeContext,
36
37
  ReportableSection: () => ReportableSection,
@@ -57,7 +58,7 @@ var Dialog = __toESM(require("@radix-ui/react-dialog"));
57
58
  var Popover = __toESM(require("@radix-ui/react-popover"));
58
59
  var import_react3 = require("@phosphor-icons/react");
59
60
  var import_date_fns = require("date-fns");
60
- var import_react4 = require("react");
61
+ var import_react4 = __toESM(require("react"));
61
62
 
62
63
  // src/provider.tsx
63
64
  var import_react2 = require("react");
@@ -89,10 +90,24 @@ var initialModalState = {
89
90
  var IssueReportingContext = (0, import_react2.createContext)(
90
91
  void 0
91
92
  );
93
+ var IssueReportingPageConfigRegistryContext = (0, import_react2.createContext)(void 0);
94
+ var hiddenMarkerStyle = {
95
+ position: "absolute",
96
+ width: "1px",
97
+ height: "1px",
98
+ padding: 0,
99
+ margin: "-1px",
100
+ overflow: "hidden",
101
+ clip: "rect(0, 0, 0, 0)",
102
+ whiteSpace: "nowrap",
103
+ border: 0
104
+ };
92
105
  var defaultIssueReportingCopy = {
93
106
  entryAriaLabel: "Report issue",
94
107
  popoverTitle: "Issue Reports",
95
108
  reportNewAction: "Report New Issue",
109
+ reportPageAction: "Report This Page",
110
+ reportSpecificAction: "Pick Specific Section",
96
111
  filtersAll: "All",
97
112
  filtersUnresolved: "Unresolved",
98
113
  filtersResolved: "Resolved",
@@ -109,6 +124,9 @@ var defaultIssueReportingCopy = {
109
124
  reportModeTitle: "Report mode is active",
110
125
  reportModeDescription: "Click a highlighted section to capture the broken surface.",
111
126
  reportModeCancelAction: "Cancel",
127
+ generalPageTargetLabel: "Current Page",
128
+ surfaceRequiredDescription: "This page requires selecting a specific section before you submit a report.",
129
+ specificSectionUnavailableDescription: "No specific sections are registered on this page right now.",
112
130
  createTitlePrefix: "Report Issue",
113
131
  editTitlePrefix: "Edit Issue",
114
132
  replyTitlePrefix: "Reply to",
@@ -173,6 +191,44 @@ function normalizeTarget(target, getPageUrl) {
173
191
  metadata: target.metadata ?? {}
174
192
  };
175
193
  }
194
+ function summarizeTarget(target) {
195
+ if (typeof target === "string") {
196
+ const normalized = target.trim();
197
+ return {
198
+ component_key: normalized,
199
+ component_label: normalized,
200
+ surface_ref: null,
201
+ metadata: {}
202
+ };
203
+ }
204
+ const key = target.componentKey.trim();
205
+ return {
206
+ component_key: key,
207
+ component_label: (target.componentLabel ?? key).trim(),
208
+ surface_ref: target.surfaceRef ?? null,
209
+ metadata: target.metadata ?? {}
210
+ };
211
+ }
212
+ function isElementVisibleForReporting(element) {
213
+ if (!element || typeof window === "undefined") {
214
+ return true;
215
+ }
216
+ let current = element;
217
+ while (current) {
218
+ if (current.getAttribute("aria-hidden") === "true") {
219
+ return false;
220
+ }
221
+ if (current instanceof HTMLElement && (current.hidden || current.inert)) {
222
+ return false;
223
+ }
224
+ const computedStyle = window.getComputedStyle(current);
225
+ if (computedStyle.display === "none" || computedStyle.visibility === "hidden" || computedStyle.visibility === "collapse") {
226
+ return false;
227
+ }
228
+ current = current.parentElement;
229
+ }
230
+ return true;
231
+ }
176
232
  function normalizeIssueTarget(issue) {
177
233
  return {
178
234
  component_key: issue.target.component_key,
@@ -254,6 +310,23 @@ function getIssueNoteLengthMessage(note, copy) {
254
310
  }
255
311
  return `${note.length}/${NOTE_MAX_LENGTH}`;
256
312
  }
313
+ function buildGeneralPageTarget(copy, registeredTargets, getPageUrl) {
314
+ return {
315
+ component_key: "general_page",
316
+ component_label: copy.generalPageTargetLabel,
317
+ page_url: resolvePageUrl(getPageUrl),
318
+ surface_ref: null,
319
+ metadata: {
320
+ capture_mode: "general_page",
321
+ registered_target_count: registeredTargets.length,
322
+ ...registeredTargets.length > 0 ? {
323
+ registered_targets: registeredTargets.map(
324
+ ({ target }) => summarizeTarget(target)
325
+ )
326
+ } : {}
327
+ }
328
+ };
329
+ }
257
330
  function useIssueReporting() {
258
331
  const context = (0, import_react2.useContext)(IssueReportingContext);
259
332
  if (!context) {
@@ -263,6 +336,25 @@ function useIssueReporting() {
263
336
  }
264
337
  return context;
265
338
  }
339
+ function IssueReportingPageConfig({
340
+ createMode
341
+ }) {
342
+ const registry = (0, import_react2.useContext)(IssueReportingPageConfigRegistryContext);
343
+ const configId = (0, import_react2.useRef)(/* @__PURE__ */ Symbol("issue-reporting-page-config"));
344
+ const markerRef = (0, import_react2.useRef)(null);
345
+ if (!registry) {
346
+ throw new Error(
347
+ "IssueReportingPageConfig must be used within an IssueReportingProvider"
348
+ );
349
+ }
350
+ (0, import_react2.useEffect)(() => {
351
+ registry.registerPageConfig(configId.current, createMode, () => markerRef.current);
352
+ return () => {
353
+ registry.unregisterPageConfig(configId.current);
354
+ };
355
+ }, [createMode, registry]);
356
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { ref: markerRef, style: hiddenMarkerStyle });
357
+ }
266
358
  function useIssueReportingStatus() {
267
359
  const { client, isEligible, scope } = useIssueReporting();
268
360
  return (0, import_react_query.useQuery)({
@@ -335,6 +427,7 @@ function IssueReportingProvider({
335
427
  getPageUrl,
336
428
  defaultScope,
337
429
  allowTenantScope = false,
430
+ defaultCreateMode = "general_page",
338
431
  copy,
339
432
  children
340
433
  }) {
@@ -352,6 +445,10 @@ function IssueReportingProvider({
352
445
  const [scope, setScope] = (0, import_react2.useState)(
353
446
  () => resolveInitialScope(defaultScope, allowTenantScope)
354
447
  );
448
+ const [pageConfigs, setPageConfigs] = (0, import_react2.useState)([]);
449
+ const [registeredTargets, setRegisteredTargets] = (0, import_react2.useState)(
450
+ []
451
+ );
355
452
  (0, import_react2.useEffect)(() => {
356
453
  setScope(resolveInitialScope(defaultScope, allowTenantScope));
357
454
  }, [allowTenantScope, defaultScope]);
@@ -361,6 +458,51 @@ function IssueReportingProvider({
361
458
  const openPopover = (0, import_react2.useCallback)(() => {
362
459
  setIsPopoverOpen(true);
363
460
  }, []);
461
+ const openCreateModal = (0, import_react2.useCallback)((target) => {
462
+ setIsPopoverOpen(false);
463
+ setIsReportMode(false);
464
+ setModalState({
465
+ isOpen: true,
466
+ mode: "create",
467
+ issueReportId: null,
468
+ issue: null,
469
+ target,
470
+ error: null,
471
+ isHydrating: false
472
+ });
473
+ }, []);
474
+ const registerPageConfig = (0, import_react2.useCallback)(
475
+ (id, createMode2, getElement) => {
476
+ setPageConfigs((current) => [
477
+ ...current.filter((entry) => entry.id !== id),
478
+ { id, createMode: createMode2, getElement }
479
+ ]);
480
+ },
481
+ []
482
+ );
483
+ const unregisterPageConfig = (0, import_react2.useCallback)((id) => {
484
+ setPageConfigs((current) => current.filter((entry) => entry.id !== id));
485
+ }, []);
486
+ const registerTarget = (0, import_react2.useCallback)(
487
+ (id, target, getElement) => {
488
+ setRegisteredTargets((current) => [
489
+ ...current.filter((entry) => entry.id !== id),
490
+ { id, target, getElement }
491
+ ]);
492
+ },
493
+ []
494
+ );
495
+ const unregisterTarget = (0, import_react2.useCallback)((id) => {
496
+ setRegisteredTargets((current) => current.filter((entry) => entry.id !== id));
497
+ }, []);
498
+ const visiblePageConfigs = pageConfigs.filter(
499
+ ({ getElement }) => isElementVisibleForReporting(getElement())
500
+ );
501
+ const visibleRegisteredTargets = registeredTargets.filter(
502
+ ({ getElement }) => isElementVisibleForReporting(getElement())
503
+ );
504
+ const createMode = visiblePageConfigs.length > 0 ? visiblePageConfigs[visiblePageConfigs.length - 1].createMode : defaultCreateMode;
505
+ const hasRegisteredTargets = visibleRegisteredTargets.length > 0;
364
506
  const enterReportMode = (0, import_react2.useCallback)(() => {
365
507
  setIsReportMode(true);
366
508
  setIsPopoverOpen(false);
@@ -372,6 +514,42 @@ function IssueReportingProvider({
372
514
  setModalState(initialModalState);
373
515
  setIsPopoverOpen(true);
374
516
  }, []);
517
+ const openPageIssueModal = (0, import_react2.useCallback)(() => {
518
+ if (!isEligible) {
519
+ return;
520
+ }
521
+ openCreateModal(
522
+ buildGeneralPageTarget(mergedCopy, visibleRegisteredTargets, getPageUrl)
523
+ );
524
+ }, [
525
+ getPageUrl,
526
+ isEligible,
527
+ mergedCopy,
528
+ openCreateModal,
529
+ visibleRegisteredTargets
530
+ ]);
531
+ const startNewIssue = (0, import_react2.useCallback)(() => {
532
+ if (!isEligible) {
533
+ return;
534
+ }
535
+ if (createMode === "surface_required") {
536
+ if (hasRegisteredTargets) {
537
+ enterReportMode();
538
+ }
539
+ return;
540
+ }
541
+ if (createMode === "surface_preferred" && hasRegisteredTargets) {
542
+ enterReportMode();
543
+ return;
544
+ }
545
+ openPageIssueModal();
546
+ }, [
547
+ createMode,
548
+ enterReportMode,
549
+ hasRegisteredTargets,
550
+ isEligible,
551
+ openPageIssueModal
552
+ ]);
375
553
  const hydrateIssueIntoModal = (0, import_react2.useCallback)(
376
554
  async (issueReportId, mode) => {
377
555
  setModalState({
@@ -430,18 +608,9 @@ function IssueReportingProvider({
430
608
  return;
431
609
  }
432
610
  const normalizedTarget = normalizeTarget(target, getPageUrl);
433
- setIsReportMode(false);
434
- setModalState({
435
- isOpen: true,
436
- mode: "create",
437
- issueReportId: null,
438
- issue: null,
439
- target: normalizedTarget,
440
- error: null,
441
- isHydrating: false
442
- });
611
+ openCreateModal(normalizedTarget);
443
612
  },
444
- [getPageUrl, isEligible]
613
+ [getPageUrl, isEligible, openCreateModal]
445
614
  );
446
615
  const contextValue = (0, import_react2.useMemo)(
447
616
  () => ({
@@ -451,9 +620,12 @@ function IssueReportingProvider({
451
620
  principalId: principalId ?? null,
452
621
  copy: mergedCopy,
453
622
  isReportMode,
623
+ hasRegisteredTargets,
454
624
  enterReportMode,
455
625
  cancelReportMode,
456
626
  selectPanel,
627
+ openPageIssueModal,
628
+ startNewIssue,
457
629
  isPopoverOpen,
458
630
  openPopover,
459
631
  closePopover,
@@ -463,7 +635,8 @@ function IssueReportingProvider({
463
635
  retryModalHydration,
464
636
  scope,
465
637
  setScope,
466
- allowTenantScope
638
+ allowTenantScope,
639
+ createMode
467
640
  }),
468
641
  [
469
642
  allowTenantScope,
@@ -471,30 +644,57 @@ function IssueReportingProvider({
471
644
  client,
472
645
  closeModal,
473
646
  closePopover,
647
+ createMode,
474
648
  enterReportMode,
649
+ hasRegisteredTargets,
475
650
  isEligible,
476
651
  isPopoverOpen,
477
652
  isReportMode,
478
653
  mergedCopy,
479
654
  modalState,
480
655
  openExistingIssueModal,
656
+ openPageIssueModal,
481
657
  openPopover,
482
658
  principalId,
483
659
  reporterRoleHint,
484
660
  retryModalHydration,
485
661
  scope,
486
- selectPanel
662
+ selectPanel,
663
+ startNewIssue
487
664
  ]
488
665
  );
489
666
  const reportModeValue = (0, import_react2.useMemo)(
490
667
  () => ({
491
668
  isReportMode,
492
669
  selectPanel,
493
- cancelReportMode
670
+ cancelReportMode,
671
+ hasRegisteredTargets,
672
+ registerTarget,
673
+ unregisterTarget
494
674
  }),
495
- [cancelReportMode, isReportMode, selectPanel]
675
+ [
676
+ cancelReportMode,
677
+ hasRegisteredTargets,
678
+ isReportMode,
679
+ registerTarget,
680
+ selectPanel,
681
+ unregisterTarget
682
+ ]
683
+ );
684
+ const pageConfigRegistryValue = (0, import_react2.useMemo)(
685
+ () => ({
686
+ registerPageConfig,
687
+ unregisterPageConfig
688
+ }),
689
+ [registerPageConfig, unregisterPageConfig]
690
+ );
691
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
692
+ IssueReportingPageConfigRegistryContext.Provider,
693
+ {
694
+ value: pageConfigRegistryValue,
695
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportModeContext.Provider, { value: reportModeValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IssueReportingContext.Provider, { value: contextValue, children }) })
696
+ }
496
697
  );
497
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReportModeContext.Provider, { value: reportModeValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IssueReportingContext.Provider, { value: contextValue, children }) });
498
698
  }
499
699
 
500
700
  // src/components.tsx
@@ -660,10 +860,13 @@ function IssueReportPopover({ children }) {
660
860
  openPopover,
661
861
  closePopover,
662
862
  enterReportMode,
863
+ openPageIssueModal,
663
864
  openExistingIssueModal,
664
865
  scope,
665
866
  setScope,
666
- allowTenantScope
867
+ allowTenantScope,
868
+ createMode,
869
+ hasRegisteredTargets
667
870
  } = useIssueReporting();
668
871
  const history = useIssueReportingHistory("all");
669
872
  const status = useIssueReportingStatus();
@@ -677,6 +880,9 @@ function IssueReportPopover({ children }) {
677
880
  [allItems]
678
881
  );
679
882
  const statusSummary = status.data ? `${status.data.open_count} open \xB7 ${status.data.recent_resolved_count} recently resolved` : "Status reflects the active scope.";
883
+ const showSectionFirst = createMode === "surface_required" || createMode === "surface_preferred" && hasRegisteredTargets;
884
+ const sectionActionDisabled = !hasRegisteredTargets;
885
+ const helperText = createMode === "surface_required" ? hasRegisteredTargets ? copy.surfaceRequiredDescription : copy.specificSectionUnavailableDescription : createMode === "surface_preferred" && !hasRegisteredTargets ? copy.specificSectionUnavailableDescription : null;
680
886
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
681
887
  Popover.Root,
682
888
  {
@@ -697,19 +903,66 @@ function IssueReportPopover({ children }) {
697
903
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "text-sm font-semibold text-slate-900", children: copy.popoverTitle }),
698
904
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "mt-1 text-xs text-slate-500", children: statusSummary })
699
905
  ] }),
700
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
701
- "button",
702
- {
703
- type: "button",
704
- className: "rounded-full bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800",
705
- onClick: () => {
706
- closePopover();
707
- enterReportMode();
708
- },
709
- children: copy.reportNewAction
710
- }
711
- )
906
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-wrap justify-end gap-2", children: showSectionFirst ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
907
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
908
+ "button",
909
+ {
910
+ type: "button",
911
+ className: cn(
912
+ "rounded-full px-3 py-2 text-xs font-semibold transition",
913
+ sectionActionDisabled ? "cursor-not-allowed bg-slate-200 text-slate-500" : "bg-slate-900 text-white hover:bg-slate-800"
914
+ ),
915
+ onClick: () => {
916
+ if (sectionActionDisabled) {
917
+ return;
918
+ }
919
+ closePopover();
920
+ enterReportMode();
921
+ },
922
+ disabled: sectionActionDisabled,
923
+ children: createMode === "surface_required" ? copy.reportNewAction : copy.reportSpecificAction
924
+ }
925
+ ),
926
+ createMode !== "surface_required" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
927
+ "button",
928
+ {
929
+ type: "button",
930
+ className: "rounded-full border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50",
931
+ onClick: () => {
932
+ closePopover();
933
+ openPageIssueModal();
934
+ },
935
+ children: copy.reportPageAction
936
+ }
937
+ ) : null
938
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
939
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
940
+ "button",
941
+ {
942
+ type: "button",
943
+ className: "rounded-full bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800",
944
+ onClick: () => {
945
+ closePopover();
946
+ openPageIssueModal();
947
+ },
948
+ children: createMode === "general_page" ? copy.reportPageAction : copy.reportNewAction
949
+ }
950
+ ),
951
+ hasRegisteredTargets ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
952
+ "button",
953
+ {
954
+ type: "button",
955
+ className: "rounded-full border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50",
956
+ onClick: () => {
957
+ closePopover();
958
+ enterReportMode();
959
+ },
960
+ children: copy.reportSpecificAction
961
+ }
962
+ ) : null
963
+ ] }) })
712
964
  ] }),
965
+ helperText ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900", children: helperText }) : null,
713
966
  status.error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
714
967
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: status.error.message || copy.statusLoadFailed }),
715
968
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -986,9 +1239,23 @@ function ReportableSection({
986
1239
  }) {
987
1240
  const reportMode = useReportMode();
988
1241
  const isSelectable = Boolean(reportMode?.isReportMode);
989
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1242
+ const targetId = import_react4.default.useRef(/* @__PURE__ */ Symbol("reportable-section"));
1243
+ const elementRef = import_react4.default.useRef(null);
1244
+ (0, import_react4.useEffect)(() => {
1245
+ if (!reportMode) {
1246
+ return;
1247
+ }
1248
+ reportMode.registerTarget(targetId.current, reportableName, () => elementRef.current);
1249
+ return () => {
1250
+ reportMode.unregisterTarget(targetId.current);
1251
+ };
1252
+ }, [reportMode, reportableName]);
1253
+ return import_react4.default.createElement(
990
1254
  Component,
991
1255
  {
1256
+ ref: (node) => {
1257
+ elementRef.current = node;
1258
+ },
992
1259
  className: cn(
993
1260
  className,
994
1261
  isSelectable && "cursor-pointer ring-2 ring-amber-400 transition hover:ring-amber-500"
@@ -1003,14 +1270,15 @@ function ReportableSection({
1003
1270
  }
1004
1271
  } : void 0,
1005
1272
  role: isSelectable ? "button" : void 0,
1006
- tabIndex: isSelectable ? 0 : void 0,
1007
- children
1008
- }
1273
+ tabIndex: isSelectable ? 0 : void 0
1274
+ },
1275
+ children
1009
1276
  );
1010
1277
  }
1011
1278
  // Annotate the CommonJS export names for ESM import in node:
1012
1279
  0 && (module.exports = {
1013
1280
  FloatingIssueReportButton,
1281
+ IssueReportingPageConfig,
1014
1282
  IssueReportingProvider,
1015
1283
  ReportModeContext,
1016
1284
  ReportableSection,
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  X
10
10
  } from "@phosphor-icons/react";
11
11
  import { formatDistanceToNow } from "date-fns";
12
- import { useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
12
+ import React2, { useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
13
13
 
14
14
  // src/provider.tsx
15
15
  import {
@@ -18,6 +18,7 @@ import {
18
18
  useContext as useContext2,
19
19
  useEffect,
20
20
  useMemo,
21
+ useRef,
21
22
  useState
22
23
  } from "react";
23
24
  import {
@@ -52,10 +53,24 @@ var initialModalState = {
52
53
  var IssueReportingContext = createContext2(
53
54
  void 0
54
55
  );
56
+ var IssueReportingPageConfigRegistryContext = createContext2(void 0);
57
+ var hiddenMarkerStyle = {
58
+ position: "absolute",
59
+ width: "1px",
60
+ height: "1px",
61
+ padding: 0,
62
+ margin: "-1px",
63
+ overflow: "hidden",
64
+ clip: "rect(0, 0, 0, 0)",
65
+ whiteSpace: "nowrap",
66
+ border: 0
67
+ };
55
68
  var defaultIssueReportingCopy = {
56
69
  entryAriaLabel: "Report issue",
57
70
  popoverTitle: "Issue Reports",
58
71
  reportNewAction: "Report New Issue",
72
+ reportPageAction: "Report This Page",
73
+ reportSpecificAction: "Pick Specific Section",
59
74
  filtersAll: "All",
60
75
  filtersUnresolved: "Unresolved",
61
76
  filtersResolved: "Resolved",
@@ -72,6 +87,9 @@ var defaultIssueReportingCopy = {
72
87
  reportModeTitle: "Report mode is active",
73
88
  reportModeDescription: "Click a highlighted section to capture the broken surface.",
74
89
  reportModeCancelAction: "Cancel",
90
+ generalPageTargetLabel: "Current Page",
91
+ surfaceRequiredDescription: "This page requires selecting a specific section before you submit a report.",
92
+ specificSectionUnavailableDescription: "No specific sections are registered on this page right now.",
75
93
  createTitlePrefix: "Report Issue",
76
94
  editTitlePrefix: "Edit Issue",
77
95
  replyTitlePrefix: "Reply to",
@@ -136,6 +154,44 @@ function normalizeTarget(target, getPageUrl) {
136
154
  metadata: target.metadata ?? {}
137
155
  };
138
156
  }
157
+ function summarizeTarget(target) {
158
+ if (typeof target === "string") {
159
+ const normalized = target.trim();
160
+ return {
161
+ component_key: normalized,
162
+ component_label: normalized,
163
+ surface_ref: null,
164
+ metadata: {}
165
+ };
166
+ }
167
+ const key = target.componentKey.trim();
168
+ return {
169
+ component_key: key,
170
+ component_label: (target.componentLabel ?? key).trim(),
171
+ surface_ref: target.surfaceRef ?? null,
172
+ metadata: target.metadata ?? {}
173
+ };
174
+ }
175
+ function isElementVisibleForReporting(element) {
176
+ if (!element || typeof window === "undefined") {
177
+ return true;
178
+ }
179
+ let current = element;
180
+ while (current) {
181
+ if (current.getAttribute("aria-hidden") === "true") {
182
+ return false;
183
+ }
184
+ if (current instanceof HTMLElement && (current.hidden || current.inert)) {
185
+ return false;
186
+ }
187
+ const computedStyle = window.getComputedStyle(current);
188
+ if (computedStyle.display === "none" || computedStyle.visibility === "hidden" || computedStyle.visibility === "collapse") {
189
+ return false;
190
+ }
191
+ current = current.parentElement;
192
+ }
193
+ return true;
194
+ }
139
195
  function normalizeIssueTarget(issue) {
140
196
  return {
141
197
  component_key: issue.target.component_key,
@@ -217,6 +273,23 @@ function getIssueNoteLengthMessage(note, copy) {
217
273
  }
218
274
  return `${note.length}/${NOTE_MAX_LENGTH}`;
219
275
  }
276
+ function buildGeneralPageTarget(copy, registeredTargets, getPageUrl) {
277
+ return {
278
+ component_key: "general_page",
279
+ component_label: copy.generalPageTargetLabel,
280
+ page_url: resolvePageUrl(getPageUrl),
281
+ surface_ref: null,
282
+ metadata: {
283
+ capture_mode: "general_page",
284
+ registered_target_count: registeredTargets.length,
285
+ ...registeredTargets.length > 0 ? {
286
+ registered_targets: registeredTargets.map(
287
+ ({ target }) => summarizeTarget(target)
288
+ )
289
+ } : {}
290
+ }
291
+ };
292
+ }
220
293
  function useIssueReporting() {
221
294
  const context = useContext2(IssueReportingContext);
222
295
  if (!context) {
@@ -226,6 +299,25 @@ function useIssueReporting() {
226
299
  }
227
300
  return context;
228
301
  }
302
+ function IssueReportingPageConfig({
303
+ createMode
304
+ }) {
305
+ const registry = useContext2(IssueReportingPageConfigRegistryContext);
306
+ const configId = useRef(/* @__PURE__ */ Symbol("issue-reporting-page-config"));
307
+ const markerRef = useRef(null);
308
+ if (!registry) {
309
+ throw new Error(
310
+ "IssueReportingPageConfig must be used within an IssueReportingProvider"
311
+ );
312
+ }
313
+ useEffect(() => {
314
+ registry.registerPageConfig(configId.current, createMode, () => markerRef.current);
315
+ return () => {
316
+ registry.unregisterPageConfig(configId.current);
317
+ };
318
+ }, [createMode, registry]);
319
+ return /* @__PURE__ */ jsx("span", { ref: markerRef, style: hiddenMarkerStyle });
320
+ }
229
321
  function useIssueReportingStatus() {
230
322
  const { client, isEligible, scope } = useIssueReporting();
231
323
  return useQuery({
@@ -298,6 +390,7 @@ function IssueReportingProvider({
298
390
  getPageUrl,
299
391
  defaultScope,
300
392
  allowTenantScope = false,
393
+ defaultCreateMode = "general_page",
301
394
  copy,
302
395
  children
303
396
  }) {
@@ -315,6 +408,10 @@ function IssueReportingProvider({
315
408
  const [scope, setScope] = useState(
316
409
  () => resolveInitialScope(defaultScope, allowTenantScope)
317
410
  );
411
+ const [pageConfigs, setPageConfigs] = useState([]);
412
+ const [registeredTargets, setRegisteredTargets] = useState(
413
+ []
414
+ );
318
415
  useEffect(() => {
319
416
  setScope(resolveInitialScope(defaultScope, allowTenantScope));
320
417
  }, [allowTenantScope, defaultScope]);
@@ -324,6 +421,51 @@ function IssueReportingProvider({
324
421
  const openPopover = useCallback(() => {
325
422
  setIsPopoverOpen(true);
326
423
  }, []);
424
+ const openCreateModal = useCallback((target) => {
425
+ setIsPopoverOpen(false);
426
+ setIsReportMode(false);
427
+ setModalState({
428
+ isOpen: true,
429
+ mode: "create",
430
+ issueReportId: null,
431
+ issue: null,
432
+ target,
433
+ error: null,
434
+ isHydrating: false
435
+ });
436
+ }, []);
437
+ const registerPageConfig = useCallback(
438
+ (id, createMode2, getElement) => {
439
+ setPageConfigs((current) => [
440
+ ...current.filter((entry) => entry.id !== id),
441
+ { id, createMode: createMode2, getElement }
442
+ ]);
443
+ },
444
+ []
445
+ );
446
+ const unregisterPageConfig = useCallback((id) => {
447
+ setPageConfigs((current) => current.filter((entry) => entry.id !== id));
448
+ }, []);
449
+ const registerTarget = useCallback(
450
+ (id, target, getElement) => {
451
+ setRegisteredTargets((current) => [
452
+ ...current.filter((entry) => entry.id !== id),
453
+ { id, target, getElement }
454
+ ]);
455
+ },
456
+ []
457
+ );
458
+ const unregisterTarget = useCallback((id) => {
459
+ setRegisteredTargets((current) => current.filter((entry) => entry.id !== id));
460
+ }, []);
461
+ const visiblePageConfigs = pageConfigs.filter(
462
+ ({ getElement }) => isElementVisibleForReporting(getElement())
463
+ );
464
+ const visibleRegisteredTargets = registeredTargets.filter(
465
+ ({ getElement }) => isElementVisibleForReporting(getElement())
466
+ );
467
+ const createMode = visiblePageConfigs.length > 0 ? visiblePageConfigs[visiblePageConfigs.length - 1].createMode : defaultCreateMode;
468
+ const hasRegisteredTargets = visibleRegisteredTargets.length > 0;
327
469
  const enterReportMode = useCallback(() => {
328
470
  setIsReportMode(true);
329
471
  setIsPopoverOpen(false);
@@ -335,6 +477,42 @@ function IssueReportingProvider({
335
477
  setModalState(initialModalState);
336
478
  setIsPopoverOpen(true);
337
479
  }, []);
480
+ const openPageIssueModal = useCallback(() => {
481
+ if (!isEligible) {
482
+ return;
483
+ }
484
+ openCreateModal(
485
+ buildGeneralPageTarget(mergedCopy, visibleRegisteredTargets, getPageUrl)
486
+ );
487
+ }, [
488
+ getPageUrl,
489
+ isEligible,
490
+ mergedCopy,
491
+ openCreateModal,
492
+ visibleRegisteredTargets
493
+ ]);
494
+ const startNewIssue = useCallback(() => {
495
+ if (!isEligible) {
496
+ return;
497
+ }
498
+ if (createMode === "surface_required") {
499
+ if (hasRegisteredTargets) {
500
+ enterReportMode();
501
+ }
502
+ return;
503
+ }
504
+ if (createMode === "surface_preferred" && hasRegisteredTargets) {
505
+ enterReportMode();
506
+ return;
507
+ }
508
+ openPageIssueModal();
509
+ }, [
510
+ createMode,
511
+ enterReportMode,
512
+ hasRegisteredTargets,
513
+ isEligible,
514
+ openPageIssueModal
515
+ ]);
338
516
  const hydrateIssueIntoModal = useCallback(
339
517
  async (issueReportId, mode) => {
340
518
  setModalState({
@@ -393,18 +571,9 @@ function IssueReportingProvider({
393
571
  return;
394
572
  }
395
573
  const normalizedTarget = normalizeTarget(target, getPageUrl);
396
- setIsReportMode(false);
397
- setModalState({
398
- isOpen: true,
399
- mode: "create",
400
- issueReportId: null,
401
- issue: null,
402
- target: normalizedTarget,
403
- error: null,
404
- isHydrating: false
405
- });
574
+ openCreateModal(normalizedTarget);
406
575
  },
407
- [getPageUrl, isEligible]
576
+ [getPageUrl, isEligible, openCreateModal]
408
577
  );
409
578
  const contextValue = useMemo(
410
579
  () => ({
@@ -414,9 +583,12 @@ function IssueReportingProvider({
414
583
  principalId: principalId ?? null,
415
584
  copy: mergedCopy,
416
585
  isReportMode,
586
+ hasRegisteredTargets,
417
587
  enterReportMode,
418
588
  cancelReportMode,
419
589
  selectPanel,
590
+ openPageIssueModal,
591
+ startNewIssue,
420
592
  isPopoverOpen,
421
593
  openPopover,
422
594
  closePopover,
@@ -426,7 +598,8 @@ function IssueReportingProvider({
426
598
  retryModalHydration,
427
599
  scope,
428
600
  setScope,
429
- allowTenantScope
601
+ allowTenantScope,
602
+ createMode
430
603
  }),
431
604
  [
432
605
  allowTenantScope,
@@ -434,30 +607,57 @@ function IssueReportingProvider({
434
607
  client,
435
608
  closeModal,
436
609
  closePopover,
610
+ createMode,
437
611
  enterReportMode,
612
+ hasRegisteredTargets,
438
613
  isEligible,
439
614
  isPopoverOpen,
440
615
  isReportMode,
441
616
  mergedCopy,
442
617
  modalState,
443
618
  openExistingIssueModal,
619
+ openPageIssueModal,
444
620
  openPopover,
445
621
  principalId,
446
622
  reporterRoleHint,
447
623
  retryModalHydration,
448
624
  scope,
449
- selectPanel
625
+ selectPanel,
626
+ startNewIssue
450
627
  ]
451
628
  );
452
629
  const reportModeValue = useMemo(
453
630
  () => ({
454
631
  isReportMode,
455
632
  selectPanel,
456
- cancelReportMode
633
+ cancelReportMode,
634
+ hasRegisteredTargets,
635
+ registerTarget,
636
+ unregisterTarget
457
637
  }),
458
- [cancelReportMode, isReportMode, selectPanel]
638
+ [
639
+ cancelReportMode,
640
+ hasRegisteredTargets,
641
+ isReportMode,
642
+ registerTarget,
643
+ selectPanel,
644
+ unregisterTarget
645
+ ]
646
+ );
647
+ const pageConfigRegistryValue = useMemo(
648
+ () => ({
649
+ registerPageConfig,
650
+ unregisterPageConfig
651
+ }),
652
+ [registerPageConfig, unregisterPageConfig]
653
+ );
654
+ return /* @__PURE__ */ jsx(
655
+ IssueReportingPageConfigRegistryContext.Provider,
656
+ {
657
+ value: pageConfigRegistryValue,
658
+ children: /* @__PURE__ */ jsx(ReportModeContext.Provider, { value: reportModeValue, children: /* @__PURE__ */ jsx(IssueReportingContext.Provider, { value: contextValue, children }) })
659
+ }
459
660
  );
460
- return /* @__PURE__ */ jsx(ReportModeContext.Provider, { value: reportModeValue, children: /* @__PURE__ */ jsx(IssueReportingContext.Provider, { value: contextValue, children }) });
461
661
  }
462
662
 
463
663
  // src/components.tsx
@@ -623,10 +823,13 @@ function IssueReportPopover({ children }) {
623
823
  openPopover,
624
824
  closePopover,
625
825
  enterReportMode,
826
+ openPageIssueModal,
626
827
  openExistingIssueModal,
627
828
  scope,
628
829
  setScope,
629
- allowTenantScope
830
+ allowTenantScope,
831
+ createMode,
832
+ hasRegisteredTargets
630
833
  } = useIssueReporting();
631
834
  const history = useIssueReportingHistory("all");
632
835
  const status = useIssueReportingStatus();
@@ -640,6 +843,9 @@ function IssueReportPopover({ children }) {
640
843
  [allItems]
641
844
  );
642
845
  const statusSummary = status.data ? `${status.data.open_count} open \xB7 ${status.data.recent_resolved_count} recently resolved` : "Status reflects the active scope.";
846
+ const showSectionFirst = createMode === "surface_required" || createMode === "surface_preferred" && hasRegisteredTargets;
847
+ const sectionActionDisabled = !hasRegisteredTargets;
848
+ const helperText = createMode === "surface_required" ? hasRegisteredTargets ? copy.surfaceRequiredDescription : copy.specificSectionUnavailableDescription : createMode === "surface_preferred" && !hasRegisteredTargets ? copy.specificSectionUnavailableDescription : null;
643
849
  return /* @__PURE__ */ jsxs(
644
850
  Popover.Root,
645
851
  {
@@ -660,19 +866,66 @@ function IssueReportPopover({ children }) {
660
866
  /* @__PURE__ */ jsx2("h3", { className: "text-sm font-semibold text-slate-900", children: copy.popoverTitle }),
661
867
  /* @__PURE__ */ jsx2("p", { className: "mt-1 text-xs text-slate-500", children: statusSummary })
662
868
  ] }),
663
- /* @__PURE__ */ jsx2(
664
- "button",
665
- {
666
- type: "button",
667
- className: "rounded-full bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800",
668
- onClick: () => {
669
- closePopover();
670
- enterReportMode();
671
- },
672
- children: copy.reportNewAction
673
- }
674
- )
869
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-wrap justify-end gap-2", children: showSectionFirst ? /* @__PURE__ */ jsxs(Fragment, { children: [
870
+ /* @__PURE__ */ jsx2(
871
+ "button",
872
+ {
873
+ type: "button",
874
+ className: cn(
875
+ "rounded-full px-3 py-2 text-xs font-semibold transition",
876
+ sectionActionDisabled ? "cursor-not-allowed bg-slate-200 text-slate-500" : "bg-slate-900 text-white hover:bg-slate-800"
877
+ ),
878
+ onClick: () => {
879
+ if (sectionActionDisabled) {
880
+ return;
881
+ }
882
+ closePopover();
883
+ enterReportMode();
884
+ },
885
+ disabled: sectionActionDisabled,
886
+ children: createMode === "surface_required" ? copy.reportNewAction : copy.reportSpecificAction
887
+ }
888
+ ),
889
+ createMode !== "surface_required" ? /* @__PURE__ */ jsx2(
890
+ "button",
891
+ {
892
+ type: "button",
893
+ className: "rounded-full border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50",
894
+ onClick: () => {
895
+ closePopover();
896
+ openPageIssueModal();
897
+ },
898
+ children: copy.reportPageAction
899
+ }
900
+ ) : null
901
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
902
+ /* @__PURE__ */ jsx2(
903
+ "button",
904
+ {
905
+ type: "button",
906
+ className: "rounded-full bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800",
907
+ onClick: () => {
908
+ closePopover();
909
+ openPageIssueModal();
910
+ },
911
+ children: createMode === "general_page" ? copy.reportPageAction : copy.reportNewAction
912
+ }
913
+ ),
914
+ hasRegisteredTargets ? /* @__PURE__ */ jsx2(
915
+ "button",
916
+ {
917
+ type: "button",
918
+ className: "rounded-full border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50",
919
+ onClick: () => {
920
+ closePopover();
921
+ enterReportMode();
922
+ },
923
+ children: copy.reportSpecificAction
924
+ }
925
+ ) : null
926
+ ] }) })
675
927
  ] }),
928
+ helperText ? /* @__PURE__ */ jsx2("div", { className: "rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900", children: helperText }) : null,
676
929
  status.error ? /* @__PURE__ */ jsxs("div", { className: "space-y-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-4 text-sm text-rose-700", children: [
677
930
  /* @__PURE__ */ jsx2("div", { children: status.error.message || copy.statusLoadFailed }),
678
931
  /* @__PURE__ */ jsx2(
@@ -949,9 +1202,23 @@ function ReportableSection({
949
1202
  }) {
950
1203
  const reportMode = useReportMode();
951
1204
  const isSelectable = Boolean(reportMode?.isReportMode);
952
- return /* @__PURE__ */ jsx2(
1205
+ const targetId = React2.useRef(/* @__PURE__ */ Symbol("reportable-section"));
1206
+ const elementRef = React2.useRef(null);
1207
+ useEffect2(() => {
1208
+ if (!reportMode) {
1209
+ return;
1210
+ }
1211
+ reportMode.registerTarget(targetId.current, reportableName, () => elementRef.current);
1212
+ return () => {
1213
+ reportMode.unregisterTarget(targetId.current);
1214
+ };
1215
+ }, [reportMode, reportableName]);
1216
+ return React2.createElement(
953
1217
  Component,
954
1218
  {
1219
+ ref: (node) => {
1220
+ elementRef.current = node;
1221
+ },
955
1222
  className: cn(
956
1223
  className,
957
1224
  isSelectable && "cursor-pointer ring-2 ring-amber-400 transition hover:ring-amber-500"
@@ -966,13 +1233,14 @@ function ReportableSection({
966
1233
  }
967
1234
  } : void 0,
968
1235
  role: isSelectable ? "button" : void 0,
969
- tabIndex: isSelectable ? 0 : void 0,
970
- children
971
- }
1236
+ tabIndex: isSelectable ? 0 : void 0
1237
+ },
1238
+ children
972
1239
  );
973
1240
  }
974
1241
  export {
975
1242
  FloatingIssueReportButton,
1243
+ IssueReportingPageConfig,
976
1244
  IssueReportingProvider,
977
1245
  ReportModeContext,
978
1246
  ReportableSection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaps-issue-reporting-react",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Shared React issue-reporting UI for Sweet Potato platform consumers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,7 +24,7 @@
24
24
  "support"
25
25
  ],
26
26
  "author": "buildooor",
27
- "license": "UNLICENSED",
27
+ "license": "MIT",
28
28
  "repository": {
29
29
  "type": "git",
30
30
  "url": "https://github.com/build000r/sweet-potato"
@@ -38,7 +38,7 @@
38
38
  "@radix-ui/react-dialog": "^1.1.15",
39
39
  "@radix-ui/react-popover": "^1.1.15",
40
40
  "date-fns": "^4.1.0",
41
- "spaps-types": "^1.1.1"
41
+ "spaps-types": "^1.1.2"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@tanstack/react-query": ">=5.0.0",
@@ -72,4 +72,4 @@
72
72
  "engines": {
73
73
  "node": ">=18.0.0"
74
74
  }
75
- }
75
+ }