@the-portland-company/devnotes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @the-portland-company/devnotes
2
+
3
+ Forge-backed DevNotes and bug reporting for server-capable React and Next.js apps.
4
+
5
+ This package is Forge-only. It does not ship a local database, SQL schema, Supabase adapter, or migration path.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @the-portland-company/devnotes
11
+ ```
12
+
13
+ Peer dependencies:
14
+
15
+ - `react` `^18 || ^19`
16
+ - `react-dom` `^18 || ^19`
17
+
18
+ ## Assumptions
19
+
20
+ - The browser package never talks to Forge directly
21
+ - The host app exposes a server-side proxy at `/api/devnotes`
22
+ - The host app authenticates users and passes a bearer token to the proxy
23
+ - Forge credentials are stored only on the server side
24
+
25
+ ## Quick start
26
+
27
+ ```bash
28
+ npx devnotes-setup
29
+ ```
30
+
31
+ Then wire the generated wrapper component to your auth token source and implement the generated proxy backend against Forge.
32
+
33
+ ## Package entrypoints
34
+
35
+ - `@the-portland-company/devnotes`
36
+ - `@the-portland-company/devnotes/next`
37
+ - `@the-portland-company/devnotes/express`
38
+ - `@the-portland-company/devnotes/styles.css`
39
+
40
+ ## What this package ships
41
+
42
+ - React UI components for in-app bug reporting and DevNotes overlays
43
+ - A browser client that talks to your host app at `/api/devnotes`
44
+ - Next.js and Express proxy helpers for server-side routing
45
+ - A `devnotes-setup` CLI that copies starter integration templates into a host app
46
+
47
+ ## Client usage
48
+
49
+ ```tsx
50
+ import {
51
+ DevNotesButton,
52
+ DevNotesProvider,
53
+ createDevNotesClient,
54
+ } from '@the-portland-company/devnotes';
55
+ import '@the-portland-company/devnotes/styles.css';
56
+
57
+ const client = createDevNotesClient({
58
+ getAuthToken: async () => session.accessToken,
59
+ });
60
+
61
+ export function AppDevNotes({ children }) {
62
+ return (
63
+ <DevNotesProvider adapter={client} user={{ id: session.user.id, email: session.user.email }}>
64
+ {children}
65
+ <DevNotesButton />
66
+ </DevNotesProvider>
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## Server helpers
72
+
73
+ - `@the-portland-company/devnotes/next`
74
+ - `@the-portland-company/devnotes/express`
75
+
76
+ These provide request-routing helpers for a host proxy. They do not implement Forge access for you; your app backend remains responsible for the Forge integration.
77
+
78
+ ## Host app requirements
79
+
80
+ - Expose a server-side proxy at `/api/devnotes`
81
+ - Authenticate the current user in the host app
82
+ - Return a bearer token from `getAuthToken()`
83
+ - Keep Forge credentials server-side only
84
+ - Translate DevNotes operations into real Forge API requests
85
+
86
+ ## Storage model
87
+
88
+ - The browser package never talks to a database directly
89
+ - The package does not persist to Supabase, Postgres, SQLite, or any local store
90
+ - Your host proxy must translate DevNotes operations into real Focus Forge API calls
91
+ - App-level Forge credentials must stay on the server side
package/bin/setup.js ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const BOLD = '\x1b[1m';
8
+ const GREEN = '\x1b[32m';
9
+ const CYAN = '\x1b[36m';
10
+ const YELLOW = '\x1b[33m';
11
+ const RESET = '\x1b[0m';
12
+
13
+ function log(message) {
14
+ console.log(message);
15
+ }
16
+
17
+ function ok(message) {
18
+ log(`${GREEN}✓${RESET} ${message}`);
19
+ }
20
+
21
+ function warn(message) {
22
+ log(`${YELLOW}⚠${RESET} ${message}`);
23
+ }
24
+
25
+ function title(message) {
26
+ log(`\n${BOLD}${CYAN}${message}${RESET}`);
27
+ }
28
+
29
+ function readJson(filePath) {
30
+ try {
31
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function detectProject(projectRoot) {
38
+ const pkg = readJson(path.join(projectRoot, 'package.json')) || {};
39
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
40
+ if (deps.next) return 'next';
41
+ if (deps.express || deps.fastify || deps.koa || deps.hono) return 'server-react';
42
+ return 'unknown';
43
+ }
44
+
45
+ function resolveTemplate(projectRoot, fileName) {
46
+ try {
47
+ const pkgDir = path.dirname(
48
+ require.resolve('@the-portland-company/devnotes/package.json', { paths: [projectRoot] })
49
+ );
50
+ return path.join(pkgDir, 'templates', fileName);
51
+ } catch {
52
+ return path.join(__dirname, '..', 'templates', fileName);
53
+ }
54
+ }
55
+
56
+ function writeIfMissing(targetPath, content) {
57
+ if (fs.existsSync(targetPath)) {
58
+ warn(`Skipped existing file: ${path.relative(process.cwd(), targetPath)}`);
59
+ return;
60
+ }
61
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
62
+ fs.writeFileSync(targetPath, content);
63
+ ok(`Created ${path.relative(process.cwd(), targetPath)}`);
64
+ }
65
+
66
+ function copyTemplate(projectRoot, templateName, destination) {
67
+ const source = resolveTemplate(projectRoot, templateName);
68
+ const content = fs.readFileSync(source, 'utf8');
69
+ writeIfMissing(path.join(projectRoot, destination), content);
70
+ }
71
+
72
+ async function main() {
73
+ const projectRoot = process.cwd();
74
+ const projectType = detectProject(projectRoot);
75
+
76
+ title('@the-portland-company/devnotes setup');
77
+ log(`Detected project type: ${BOLD}${projectType}${RESET}`);
78
+
79
+ if (projectType === 'next') {
80
+ copyTemplate(projectRoot, 'NextDevNotesRoute.ts', 'app/api/devnotes/[...slug]/route.ts');
81
+ copyTemplate(projectRoot, 'DevNotesWrapper.tsx', 'src/components/DevNotesWrapper.tsx');
82
+ } else if (projectType === 'server-react') {
83
+ copyTemplate(projectRoot, 'ExpressDevNotesProxy.ts', 'src/server/devnotesProxy.ts');
84
+ copyTemplate(projectRoot, 'DevNotesWrapper.tsx', 'src/components/DevNotesWrapper.tsx');
85
+ } else {
86
+ warn('Could not detect Next.js or a server-capable React app.');
87
+ warn('Copy the templates manually from node_modules/@the-portland-company/devnotes/templates.');
88
+ }
89
+
90
+ title('Required configuration');
91
+ log('- Add a server-side Forge integration behind `/api/devnotes`.');
92
+ log('- Provide a host auth token via `getAuthToken()` in `DevNotesWrapper.tsx`.');
93
+ log('- Set server env vars like `FOCUS_FORGE_BASE_URL`, `FOCUS_FORGE_PAT`, and `FOCUS_FORGE_PROJECT_NAME`.');
94
+ log('- Import `@the-portland-company/devnotes/styles.css` once in your client bundle.');
95
+ }
96
+
97
+ main().catch((error) => {
98
+ console.error(error);
99
+ process.exit(1);
100
+ });
@@ -0,0 +1,261 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { D as DevNotesClientAdapter, a as DevNotesUser, b as DevNotesConfig, B as BugReport, c as BugReportType, T as TaskList, d as BugReportCreator, e as DevNotesCapabilities, f as DevNotesAppLinkStatus, N as NotifyEvent, A as AiProvider, g as DevNotesRole, h as DevNotesClientOptions, i as BugCaptureContext } from './types-xqGNcAbZ.mjs';
4
+ export { j as AiAssistResult, k as AiConversationMessage, l as BugReportCreateData, m as BugReportMessage, n as DevNotesLinkAppInput } from './types-xqGNcAbZ.mjs';
5
+
6
+ type DevNotesContextValue = {
7
+ isEnabled: boolean;
8
+ setIsEnabled: (enabled: boolean) => void;
9
+ showBugsAlways: boolean;
10
+ setShowBugsAlways: (show: boolean) => void;
11
+ hideResolvedClosed: boolean;
12
+ setHideResolvedClosed: (hide: boolean) => void;
13
+ bugReports: BugReport[];
14
+ bugReportTypes: BugReportType[];
15
+ taskLists: TaskList[];
16
+ userProfiles: Record<string, BugReportCreator>;
17
+ unreadCounts: Record<string, number>;
18
+ currentPageBugReports: BugReport[];
19
+ collaborators: BugReportCreator[];
20
+ loadBugReports: () => Promise<void>;
21
+ loadBugReportTypes: () => Promise<void>;
22
+ loadTaskLists: () => Promise<void>;
23
+ createBugReport: (report: Omit<BugReport, 'id' | 'created_at' | 'updated_at' | 'created_by' | 'resolved_at' | 'resolved_by'>) => Promise<BugReport | null>;
24
+ updateBugReport: (id: string, updates: Partial<BugReport>) => Promise<BugReport | null>;
25
+ deleteBugReport: (id: string) => Promise<boolean>;
26
+ createTaskList: (name: string) => Promise<TaskList | null>;
27
+ addBugReportType: (name: string) => Promise<BugReportType | null>;
28
+ deleteBugReportType: (id: string) => Promise<boolean>;
29
+ loadUnreadCounts: () => Promise<void>;
30
+ markMessagesAsRead: (reportId: string, messageIds: string[]) => Promise<void>;
31
+ user: DevNotesUser;
32
+ adapter: DevNotesClientAdapter;
33
+ capabilities: DevNotesCapabilities;
34
+ appLinkStatus: DevNotesAppLinkStatus | null;
35
+ refreshCapabilities: () => Promise<void>;
36
+ refreshAppLinkStatus: () => Promise<void>;
37
+ onNotify?: (event: NotifyEvent) => void;
38
+ aiProvider?: AiProvider;
39
+ requireAi: boolean;
40
+ role: DevNotesRole;
41
+ loading: boolean;
42
+ error: string | null;
43
+ dotContainer: HTMLDivElement | null;
44
+ compensate: (viewportX: number, viewportY: number) => {
45
+ x: number;
46
+ y: number;
47
+ };
48
+ };
49
+ type DevNotesProviderProps = {
50
+ adapter: DevNotesClientAdapter;
51
+ user: DevNotesUser;
52
+ config?: DevNotesConfig;
53
+ children: ReactNode;
54
+ };
55
+ declare function DevNotesProvider({ adapter, user, config, children }: DevNotesProviderProps): react_jsx_runtime.JSX.Element;
56
+ declare function useDevNotes(): DevNotesContextValue;
57
+
58
+ type DevNotesButtonProps = {
59
+ /** Position of the floating button */
60
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
61
+ /** Called when user clicks "View Tasks" in the menu. If omitted, a built-in task panel opens. */
62
+ onViewTasks?: () => void;
63
+ /** Called when user clicks "Settings" in the menu */
64
+ onSettings?: () => void;
65
+ /** Custom icon component for the menu trigger */
66
+ icon?: React.ComponentType<{
67
+ size?: number;
68
+ color?: string;
69
+ }>;
70
+ /** Optional: ID of a report to open immediately */
71
+ openReportId?: string | null;
72
+ /** Called when the opened report modal is closed */
73
+ onOpenReportClose?: () => void;
74
+ /** Called when user wants to navigate to a report's page (used by built-in task list) */
75
+ onNavigateToPage?: (pageUrl: string, reportId: string) => void;
76
+ };
77
+ declare function DevNotesButton({ position, onViewTasks, onSettings, icon, openReportId, onOpenReportClose, onNavigateToPage, }: DevNotesButtonProps): react_jsx_runtime.JSX.Element | null;
78
+
79
+ type DevNotesOverlayProps = {
80
+ /** Optional: ID of a report to open immediately */
81
+ openReportId?: string | null;
82
+ /** Called when the opened report is closed */
83
+ onOpenReportClose?: () => void;
84
+ };
85
+ declare function DevNotesOverlay({ openReportId, onOpenReportClose, }?: DevNotesOverlayProps): react_jsx_runtime.JSX.Element | null;
86
+
87
+ type DevNotesMenuProps = {
88
+ /** Called when user clicks "View Tasks" */
89
+ onViewTasks?: () => void;
90
+ /** Called when user clicks "Settings" */
91
+ onSettings?: () => void;
92
+ /** Custom icon component for the menu trigger */
93
+ icon?: React.ComponentType<{
94
+ size?: number;
95
+ color?: string;
96
+ }>;
97
+ /** Position of the parent button — controls dropdown alignment */
98
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
99
+ /** Direction the dropdown opens — default 'down' */
100
+ dropdownDirection?: 'up' | 'down';
101
+ };
102
+ declare function DevNotesMenu({ onViewTasks, onSettings, icon: IconComponent, position, dropdownDirection }: DevNotesMenuProps): react_jsx_runtime.JSX.Element | null;
103
+
104
+ type DevNotesFormProps = {
105
+ pageUrl: string;
106
+ xPosition?: number;
107
+ yPosition?: number;
108
+ targetSelector?: string | null;
109
+ targetRelativeX?: number | null;
110
+ targetRelativeY?: number | null;
111
+ existingReport?: BugReport | null;
112
+ onSave: (report: BugReport) => void;
113
+ onCancel: () => void;
114
+ onDelete?: () => void;
115
+ };
116
+ declare function DevNotesForm({ pageUrl, xPosition, yPosition, targetSelector, targetRelativeX, targetRelativeY, existingReport, onSave, onCancel, onDelete, }: DevNotesFormProps): react_jsx_runtime.JSX.Element;
117
+
118
+ type DevNotesDotProps = {
119
+ report: BugReport;
120
+ };
121
+ declare function DevNotesDot({ report }: DevNotesDotProps): react_jsx_runtime.JSX.Element | null;
122
+
123
+ type DevNotesDiscussionProps = {
124
+ report: BugReport;
125
+ };
126
+ declare function DevNotesDiscussion({ report }: DevNotesDiscussionProps): react_jsx_runtime.JSX.Element;
127
+
128
+ type DevNotesTaskListProps = {
129
+ /** Called when the user wants to navigate to the page where a report was filed */
130
+ onNavigateToPage?: (pageUrl: string, reportId: string) => void;
131
+ /** Called when the close/back button is clicked (if rendered as an overlay) */
132
+ onClose?: () => void;
133
+ /** Title shown at the top */
134
+ title?: string;
135
+ };
136
+ declare function DevNotesTaskList({ onNavigateToPage, onClose, title, }: DevNotesTaskListProps): react_jsx_runtime.JSX.Element;
137
+
138
+ declare function createDevNotesClient(options: DevNotesClientOptions): DevNotesClientAdapter;
139
+
140
+ type BugAnchorMetadata = {
141
+ targetSelector: string | null;
142
+ targetRelativeX: number | null;
143
+ targetRelativeY: number | null;
144
+ };
145
+ type BugPositionPayload = BugAnchorMetadata & {
146
+ x: number;
147
+ y: number;
148
+ };
149
+ /**
150
+ * Normalize a page URL for bug report storage.
151
+ * Strips hash fragments and trailing slashes. Preserves all query params.
152
+ */
153
+ declare const normalizePageUrl: (value: string) => string;
154
+ declare const calculateBugPositionFromPoint: ({ clientX, clientY, elementsToIgnore, }: {
155
+ clientX: number;
156
+ clientY: number;
157
+ elementsToIgnore?: Array<HTMLElement | null | undefined>;
158
+ }) => BugPositionPayload;
159
+ /**
160
+ * Resolve a bug report's position to viewport coordinates for `position: fixed` rendering.
161
+ *
162
+ * Priority 1: Find the anchored element by CSS selector and calculate viewport-relative
163
+ * position from its current bounding rect. This makes the dot track the element.
164
+ * Priority 2: Fall back to stored page coordinates converted to viewport coordinates.
165
+ */
166
+ declare const resolveBugReportCoordinates: (report: BugReport) => {
167
+ x: number;
168
+ y: number;
169
+ } | null;
170
+
171
+ declare function deriveRouteLabelFromUrl(rawUrl: string): string;
172
+ declare function detectBrowserName(userAgent: string): string;
173
+ declare function buildCaptureContext(pageUrl: string): BugCaptureContext | null;
174
+
175
+ type AiFixPayload = {
176
+ source: string;
177
+ copied_at: string;
178
+ report: {
179
+ id: string | null;
180
+ title: string | null;
181
+ status: BugReport['status'];
182
+ severity: BugReport['severity'];
183
+ task_list_id: string | null;
184
+ types: string[];
185
+ type_names: string[];
186
+ approved: boolean;
187
+ ai_ready: boolean;
188
+ };
189
+ narrative: {
190
+ description: string | null;
191
+ expected_behavior: string | null;
192
+ actual_behavior: string | null;
193
+ ai_description: string | null;
194
+ response: string | null;
195
+ };
196
+ context: {
197
+ page_url: string;
198
+ route_label: string;
199
+ x_position: number;
200
+ y_position: number;
201
+ target_selector: string | null;
202
+ target_relative_x: number | null;
203
+ target_relative_y: number | null;
204
+ capture_context: BugCaptureContext | null;
205
+ };
206
+ workflow: {
207
+ assigned_to: string | null;
208
+ resolved_by: string | null;
209
+ created_by: string;
210
+ created_at: string | null;
211
+ updated_at: string | null;
212
+ };
213
+ };
214
+ type BuildAiFixPayloadParams = {
215
+ source?: string;
216
+ copiedAt?: string;
217
+ report: {
218
+ id?: string | null;
219
+ title?: string | null;
220
+ status: BugReport['status'];
221
+ severity: BugReport['severity'];
222
+ taskListId?: string | null;
223
+ types: string[];
224
+ typeNames: string[];
225
+ approved: boolean;
226
+ aiReady: boolean;
227
+ };
228
+ narrative: {
229
+ description?: string | null;
230
+ expectedBehavior?: string | null;
231
+ actualBehavior?: string | null;
232
+ aiDescription?: string | null;
233
+ response?: string | null;
234
+ };
235
+ context: {
236
+ pageUrl: string;
237
+ routeLabel: string;
238
+ xPosition: number;
239
+ yPosition: number;
240
+ targetSelector?: string | null;
241
+ targetRelativeX?: number | null;
242
+ targetRelativeY?: number | null;
243
+ captureContext?: BugCaptureContext | null;
244
+ };
245
+ workflow: {
246
+ assignedTo?: string | null;
247
+ resolvedBy?: string | null;
248
+ createdBy: string;
249
+ createdAt?: string | null;
250
+ updatedAt?: string | null;
251
+ };
252
+ };
253
+ declare function buildAiFixPayload(params: BuildAiFixPayloadParams): AiFixPayload;
254
+ declare function formatAiFixPayloadForCopy(payload: AiFixPayload): string;
255
+
256
+ declare const useBugReportPosition: (report: BugReport | null) => {
257
+ x: number;
258
+ y: number;
259
+ } | null;
260
+
261
+ export { type AiFixPayload, AiProvider, BugCaptureContext, BugReport, BugReportCreator, BugReportType, type BuildAiFixPayloadParams, DevNotesAppLinkStatus, DevNotesButton, DevNotesCapabilities, DevNotesClientOptions, DevNotesConfig, DevNotesDiscussion, DevNotesDot, DevNotesForm, DevNotesMenu, DevNotesOverlay, DevNotesProvider, DevNotesRole, DevNotesTaskList, DevNotesUser, NotifyEvent, TaskList, buildAiFixPayload, buildCaptureContext, calculateBugPositionFromPoint, createDevNotesClient, deriveRouteLabelFromUrl, detectBrowserName, formatAiFixPayloadForCopy, normalizePageUrl, resolveBugReportCoordinates, useBugReportPosition, useDevNotes };