@ldlework/feedback 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dustin Lacewell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,227 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿ’ฌ feedback
4
+
5
+ **Drag-and-drop feedback, right on your live React page.**
6
+
7
+ Reviewers drop sticky notes onto the running app, draw arrows at exactly what they mean, and send it all back as one JSON file. A built-in review mode steps through every note.
8
+
9
+ [![npm](https://img.shields.io/npm/v/@ldlework/feedback?color=%23e6a817&label=npm)](https://www.npmjs.com/package/@ldlework/feedback)
10
+ [![license](https://img.shields.io/npm/l/@ldlework/feedback?color=%23e6a817)](./LICENSE)
11
+ [![types](https://img.shields.io/npm/types/@ldlework/feedback?color=%23e6a817)](https://www.npmjs.com/package/@ldlework/feedback)
12
+ ![react](https://img.shields.io/badge/React-18%20%7C%2019-%23e6a817)
13
+
14
+ **[Live demo](https://feedback.ldlework.com)** ยท **[Docs](https://feedback.ldlework.com/docs)** ยท **[npm](https://www.npmjs.com/package/@ldlework/feedback)**
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ `@ldlework/feedback` is a drop-in annotation overlay for React. Mount one provider and one component and a dock appears in the corner: drag a note onto the page, type what you're flagging, and draw an arrow at any element on the page. Notes save to `localStorage` as you go and export to a single file โ€” arrows carry a CSS path to their target so a developer can find it without the picture.
21
+
22
+ - ๐ŸŽฏ **Drag to annotate** โ€” drop notes anywhere, draw arrows from a note's edge straight at what you mean.
23
+ - ๐Ÿ“ **Pinned to content** โ€” positions are stored in *content coordinates*, so notes stay glued to your layout across every window size.
24
+ - ๐Ÿงฑ **Inner scroll panes** โ€” wrap a pane's content in a region so notes scroll and clip with it, not just the window.
25
+ - ๐Ÿงญ **Review mode** โ€” step through every note; it routes to the right page and section and scrolls it into view.
26
+ - ๐ŸŽจ **Themeable** โ€” one stylesheet, every colour a CSS variable; re-skin it all from a single `--fb-hue`.
27
+ - ๐Ÿ’พ **Save to a file** โ€” export/import one JSON document; nothing leaves the browser unless you send it.
28
+ - ๐Ÿ“ฎ **Submit to a server** โ€” point `submit` at an endpoint (or hand it a function) and the dock grows a send button.
29
+ - ๐Ÿชถ **Zero runtime dependencies** โ€” ships ESM + types, router-agnostic, no CSS framework required.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pnpm add @ldlework/feedback
35
+ # npm i @ldlework/feedback ยท yarn add @ldlework/feedback
36
+ ```
37
+
38
+ `react` and `react-dom` (v18 or v19) are peer dependencies. Import the stylesheet once, anywhere in your app:
39
+
40
+ ```ts
41
+ import '@ldlework/feedback/styles.css'
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ Wrap your tree in `FeedbackProvider` and mount `FeedbackLayer` once inside it. On a page that scrolls the window, that's the entire integration.
47
+
48
+ ```tsx
49
+ import { FeedbackProvider, FeedbackLayer } from '@ldlework/feedback'
50
+ import '@ldlework/feedback/styles.css'
51
+
52
+ export function App() {
53
+ return (
54
+ <FeedbackProvider>
55
+ <YourApp />
56
+ <FeedbackLayer />
57
+ </FeedbackProvider>
58
+ )
59
+ }
60
+ ```
61
+
62
+ ## Inner scroll & sections
63
+
64
+ When your content scrolls inside a panel rather than the window, wrap that content in a `<FeedbackRegion>` โ€” inside whatever scroll container you already have. Notes then live in the region: they ride along with the scroll and clip with the pane.
65
+
66
+ ```tsx
67
+ import { FeedbackRegion } from '@ldlework/feedback'
68
+
69
+ function DocumentPane() {
70
+ return (
71
+ <div style={{ overflow: 'auto', height: '100vh' }}>
72
+ <FeedbackRegion anchorX="left">
73
+ {/* content */}
74
+ </FeedbackRegion>
75
+ </div>
76
+ )
77
+ }
78
+ ```
79
+
80
+ A `section` is an optional sub-scope of a page โ€” notes tagged with one only show while that section is active, so a carousel step or tab doesn't leak its notes onto its siblings. Provide `onReveal` so review can switch to a section before it scrolls to a note:
81
+
82
+ ```tsx
83
+ <FeedbackRegion section={currentStepId} onReveal={(stepId) => goToStep(stepId)}>
84
+ {steps[currentStepId]}
85
+ </FeedbackRegion>
86
+ ```
87
+
88
+ ## Submitting to a server
89
+
90
+ By default feedback leaves the browser only as a downloaded file. Give the provider a `submit` target and the dock adds a send button that delivers the document over the network instead โ€” the button reports sending, sent, and failure states, and file export stays available alongside it.
91
+
92
+ An endpoint URL is the whole integration โ€” the document is POSTed there as JSON with a `submittedAt` timestamp:
93
+
94
+ ```tsx
95
+ <FeedbackProvider submit="https://api.example.com/feedback">
96
+ ```
97
+
98
+ Need auth headers or a different method? Build the submitter with `endpointSubmitter`:
99
+
100
+ ```tsx
101
+ import { endpointSubmitter } from '@ldlework/feedback'
102
+
103
+ <FeedbackProvider
104
+ submit={endpointSubmitter('https://api.example.com/feedback', {
105
+ headers: { Authorization: `Bearer ${token}` },
106
+ credentials: 'include',
107
+ })}
108
+ >
109
+ ```
110
+
111
+ Or supply any `(doc: FeedbackDoc) => Promise<void>` for full control โ€” an SDK call, a queue, whatever. Throw (or reject) to signal failure; the dock shows the error state and the reviewer can retry.
112
+
113
+ ## The coordinate model
114
+
115
+ A note remembers where it belongs in the **content**, not on the screen. Its `y` is measured from the top of the content; its `x` is measured from an **anchor line** chosen by `anchorX`:
116
+
117
+ - **`left`** โ€” x from the content's left edge (absolute).
118
+ - **`center`** *(default)* โ€” x signed from the horizontal centre line, so a note dropped on a centred column stays glued to it as the window narrows.
119
+ - **`right`** โ€” x from the content's right edge.
120
+
121
+ Because positions are stored this way, they survive a resize the way the content itself does. A **region** is the element those coordinates live in โ€” the document/viewport by default, or the content you wrapped in a `<FeedbackRegion>`.
122
+
123
+ ## Router integration
124
+
125
+ Review mode walks notes across pages. With no configuration it reads `location.pathname` and navigates through the History API. If you use a router, hand it an adapter:
126
+
127
+ ```tsx
128
+ import { useMemo } from 'react'
129
+ import { FeedbackProvider, type FeedbackNavigation } from '@ldlework/feedback'
130
+ import { useLocation, useNavigate } from 'react-router-dom'
131
+
132
+ function Providers({ children }: { children: React.ReactNode }) {
133
+ const { pathname } = useLocation()
134
+ const navigate = useNavigate()
135
+ const navigation = useMemo<FeedbackNavigation>(
136
+ () => ({ page: pathname, navigate: (to) => navigate(to) }),
137
+ [pathname, navigate],
138
+ )
139
+ return <FeedbackProvider navigation={navigation}>{children}</FeedbackProvider>
140
+ }
141
+ ```
142
+
143
+ ## Theming
144
+
145
+ Every class is `fb-` prefixed and every colour is a `--fb-*` custom property scoped to `.fb-root`, so nothing leaks in or out. Re-skin the whole overlay from one knob:
146
+
147
+ ```css
148
+ /* Shift the accent and the chrome together to a cool blue. */
149
+ .fb-root { --fb-hue: 220; }
150
+ ```
151
+
152
+ Or override individual tokens:
153
+
154
+ ```css
155
+ .fb-root {
156
+ --fb-accent: #7c5cff;
157
+ --fb-accent-contrast: #ffffff;
158
+ --fb-surface-overlay: #1b1b2b;
159
+ --fb-radius: 10px;
160
+ }
161
+ ```
162
+
163
+ You can also scope a theme to one provider โ€” `className` and `style` are applied to every portal root, including the ones the overlay teleports to.
164
+
165
+ Key tokens: `--fb-hue`, `--fb-accent`, `--fb-accent-hover`, `--fb-accent-contrast`, `--fb-surface`, `--fb-surface-raised`, `--fb-surface-overlay`, `--fb-surface-sunken`, `--fb-border`, `--fb-border-strong`, `--fb-text`, `--fb-text-secondary`, `--fb-text-muted`, `--fb-success`, `--fb-danger`, `--fb-radius`, `--fb-shadow`, `--fb-font`.
166
+
167
+ ## API
168
+
169
+ ### `<FeedbackProvider>`
170
+
171
+ | Prop | Type | Default โ€” description |
172
+ | --- | --- | --- |
173
+ | `storageKey` | `string` | `"feedback"` โ€” localStorage key the document is saved under. |
174
+ | `fileName` | `string` | `"feedback.json"` โ€” suggested name for exported files. |
175
+ | `navigation` | `FeedbackNavigation` | History-API adapter โ€” `{ page, navigate }` for router integration. |
176
+ | `submit` | `string \| FeedbackSubmitter` | Unset โ€” an endpoint URL (document POSTed as JSON) or a custom async submitter; adds a send button to the dock. |
177
+ | `defaultAnchorX` | `AnchorX` | `"center"` โ€” x anchoring for pages that mount no region. |
178
+ | `className` | `string` | Extra class on every portal root โ€” e.g. a theme scope. |
179
+ | `style` | `CSSProperties` | CSS-variable overrides applied to every portal root. |
180
+
181
+ ### `<FeedbackRegion>`
182
+
183
+ | Prop | Type | Description |
184
+ | --- | --- | --- |
185
+ | `anchorX` | `'left' \| 'center' \| 'right'` | How x is anchored within this region. |
186
+ | `section` | `string \| null` | A sub-scope so notes on one view don't leak onto siblings. |
187
+ | `onReveal` | `(section: string) => void` | How review reveals a section before scrolling to a note. |
188
+ | `className`, `style` | | Applied to the wrapper element (`position` is reserved). |
189
+
190
+ ### Also exported
191
+
192
+ `FeedbackLayer` (the overlay, no props) ยท types `FeedbackProviderProps`, `FeedbackRegionProps`, `FeedbackNavigation`, `FeedbackSubmitter`, `EndpointOptions`, `AnchorX`, `Region`, `FeedbackNote`, `FeedbackEdge`, `FeedbackDoc`, `EdgeTarget`, `Port` ยท helpers `endpointSubmitter(url, options)`, `normalizeDoc(json)`, and `EMPTY_DOC` for reading an untrusted file back into a document.
193
+
194
+ ## File format
195
+
196
+ Export writes one JSON file โ€” an `exportedAt` timestamp plus the notes and edges. Each arrow carries a `target` (a CSS path and a text excerpt of the element it points at):
197
+
198
+ ```json
199
+ {
200
+ "exportedAt": "2026-07-08T12:00:00.000Z",
201
+ "notes": [
202
+ { "id": "โ€ฆ", "page": "/pricing", "section": null,
203
+ "x": -120, "y": 480, "text": "This CTA is unclear", "resolved": false }
204
+ ],
205
+ "edges": [
206
+ { "id": "โ€ฆ", "page": "/pricing", "section": null, "noteId": "โ€ฆ",
207
+ "port": "right", "x": 40, "y": 500,
208
+ "target": { "selector": "section.hero > button.cta", "text": "Start free" } }
209
+ ]
210
+ }
211
+ ```
212
+
213
+ ## Contributing
214
+
215
+ This is a pnpm workspace: the library at the root, the demo site in `site/`.
216
+
217
+ ```bash
218
+ pnpm install
219
+ pnpm build # build the library (tsup โ†’ dist/)
220
+ pnpm --filter feedback-site dev # run the demo + docs site
221
+ ```
222
+
223
+ The site builds the library straight from source, so there's no build-then-run dance during development.
224
+
225
+ ## License
226
+
227
+ MIT ยฉ [Dustin Lacewell](https://github.com/dustinlacewell)
@@ -0,0 +1,190 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
3
+
4
+ /** Which knob on a note's border an edge grows from. */
5
+ type Port = 'left' | 'bottom' | 'right';
6
+
7
+ /**
8
+ * The coordinate system.
9
+ *
10
+ * A note stores its position in *content* coordinates โ€” independent of
11
+ * how the page is scrolled or where its region sits on screen. `y` is
12
+ * measured from the content top; `x` is measured from an **anchor line**
13
+ * chosen by `anchorX`, so positions survive a resize the way the content
14
+ * itself does:
15
+ *
16
+ * - `left` โ€” x from the content's left edge (absolute).
17
+ * - `center` โ€” x signed from the horizontal centre line. A note dropped
18
+ * on a centred column stays glued to it as the window narrows. This is
19
+ * the default, because most sites centre their main content.
20
+ * - `right` โ€” x from the content's right edge.
21
+ *
22
+ * A **region** is the element those coordinates live in: either the
23
+ * document/viewport (the default) or the content a page wrapped in a
24
+ * `<FeedbackRegion>`. This module is the single seam that converts a live
25
+ * pointer into stored coordinates and back; every part above it stays
26
+ * origin-agnostic.
27
+ */
28
+ type AnchorX = 'left' | 'center' | 'right';
29
+ interface Region {
30
+ /** The bound content element, or null for the document/viewport. */
31
+ element: HTMLElement | null;
32
+ /** Which edge of the content x is measured from. */
33
+ anchorX: AnchorX;
34
+ }
35
+
36
+ /**
37
+ * The feedback document: notes and edges, plus the coercion that turns
38
+ * untrusted data (a loaded file, or restored storage) into a valid one.
39
+ *
40
+ * Notes are the sticky notes a reviewer drops; edges are arrows drawn
41
+ * from a note's knob to a free point. Coordinates are stored in content
42
+ * space (see `coordinates.ts`) so they track the layout across window
43
+ * sizes. Pure data โ€” the React binding lives in `useFeedbackDoc`.
44
+ */
45
+
46
+ interface FeedbackNote {
47
+ id: string;
48
+ /** Top-level scope the note was dropped on (a route/path); revisited during review. */
49
+ page: string;
50
+ /** Sub-scope within the page (e.g. a carousel step); null spans the page. */
51
+ section: string | null;
52
+ /** Stored in content coordinates โ€” see `coordinates.ts`. */
53
+ x: number;
54
+ y: number;
55
+ text: string;
56
+ /** Marked done during review; kept, not deleted. */
57
+ resolved: boolean;
58
+ }
59
+ /** The page element an arrow lands on, addressed for later identification. */
60
+ interface EdgeTarget {
61
+ /** A CSS path carrying every class at each level, e.g. `article.prose > p.lead`. */
62
+ selector: string;
63
+ /** A short text excerpt from the element, or null when it has none. */
64
+ text: string | null;
65
+ }
66
+ interface FeedbackEdge {
67
+ id: string;
68
+ page: string;
69
+ section: string | null;
70
+ /** The note this arrow's tail is pinned to. */
71
+ noteId: string;
72
+ /** Which of the note's knobs the tail grows from. */
73
+ port: Port;
74
+ /** The arrowhead, in the same content coordinates as a note. */
75
+ x: number;
76
+ y: number;
77
+ /** What the arrowhead points at, captured on draw and re-aim. */
78
+ target: EdgeTarget | null;
79
+ }
80
+ interface FeedbackDoc {
81
+ notes: FeedbackNote[];
82
+ edges: FeedbackEdge[];
83
+ }
84
+ declare const EMPTY_DOC: FeedbackDoc;
85
+ /**
86
+ * Coerce untrusted data into a document. Accepts a `{ notes, edges }`
87
+ * envelope, or a bare notes array from an older export. Edges whose note
88
+ * is gone are dropped.
89
+ */
90
+ declare function normalizeDoc(data: unknown): FeedbackDoc;
91
+
92
+ /**
93
+ * How feedback learns which "page" it's on and how to move between them.
94
+ *
95
+ * A note remembers the page it was left on so review can return to it.
96
+ * Apps that route client-side (React Router, TanStack Router, Next) pass
97
+ * their own adapter so review navigates through the router rather than
98
+ * reloading. With no adapter, feedback reads `location.pathname` and
99
+ * navigates by pushing history state โ€” enough for a single-page site.
100
+ */
101
+ interface FeedbackNavigation {
102
+ /** The current page identity โ€” the scope a new note is filed under. */
103
+ page: string;
104
+ /** Move to a page so review can reveal a note left there. */
105
+ navigate: (page: string) => void;
106
+ }
107
+
108
+ /**
109
+ * Submission: the network counterpart to file export. A page that has
110
+ * somewhere to send feedback configures `<FeedbackProvider submit>` with
111
+ * either an endpoint URL โ€” the document is POSTed there as JSON โ€” or its
112
+ * own async function for anything richer (auth flows, an SDK, a queue).
113
+ */
114
+
115
+ /** Delivers a document to wherever feedback is collected. Throw to signal failure. */
116
+ type FeedbackSubmitter = (doc: FeedbackDoc) => Promise<void>;
117
+ interface EndpointOptions {
118
+ /** HTTP method. Default `POST`. */
119
+ method?: string;
120
+ /** Extra headers merged over the JSON content type โ€” e.g. authorization. */
121
+ headers?: Record<string, string>;
122
+ /** Passed through to `fetch`, for cookie-authenticated endpoints. */
123
+ credentials?: RequestCredentials;
124
+ }
125
+ /** Send the document as a JSON request to `url`; any non-2xx response is a failure. */
126
+ declare function endpointSubmitter(url: string, options?: EndpointOptions): FeedbackSubmitter;
127
+
128
+ interface FeedbackProviderProps {
129
+ children: ReactNode;
130
+ /** localStorage key the document is saved under. Default `"feedback"`. */
131
+ storageKey?: string;
132
+ /** Suggested name for exported files. Default `"feedback.json"`. */
133
+ fileName?: string;
134
+ /**
135
+ * Seeds the document the first time `storageKey` is used, so a page can
136
+ * ship with notes already on it โ€” a live demo, or a review handed back
137
+ * to its author. Ignored once anything has been stored under the key.
138
+ */
139
+ initialDoc?: FeedbackDoc;
140
+ /** Router adapter. Default reads `location.pathname`. */
141
+ navigation?: FeedbackNavigation;
142
+ /**
143
+ * Where "send feedback" delivers the document: an endpoint URL (the
144
+ * document is POSTed there as JSON) or a custom async submitter.
145
+ * Omitted, the dock offers file export only.
146
+ */
147
+ submit?: string | FeedbackSubmitter;
148
+ /** How x is anchored on pages that mount no region. Default `"center"`. */
149
+ defaultAnchorX?: AnchorX;
150
+ /** Extra class on every portal root โ€” e.g. a theme scope. */
151
+ className?: string;
152
+ /** CSS-variable overrides applied to every portal root. */
153
+ style?: CSSProperties;
154
+ }
155
+ declare function FeedbackProvider({ children, storageKey, fileName, navigation, defaultAnchorX, className, style, initialDoc, submit, }: FeedbackProviderProps): react.JSX.Element;
156
+
157
+ /**
158
+ * The always-on feedback layer. Mount it once inside a
159
+ * `<FeedbackProvider>` and it draws itself: the overlay of notes and
160
+ * arrows portals into the active region; the dock, review bar, and help
161
+ * pin to the viewport. Wiring only โ€” every behaviour lives in its own
162
+ * delegate.
163
+ */
164
+ declare function FeedbackLayer(): react.JSX.Element;
165
+
166
+ interface FeedbackRegionProps {
167
+ children: ReactNode;
168
+ /** How x is anchored within this region. Overrides the provider default. */
169
+ anchorX?: AnchorX;
170
+ /** A sub-scope so notes on one view don't leak onto its siblings. */
171
+ section?: string | null;
172
+ /** How review asks this page to reveal a section before scrolling to a note. */
173
+ onReveal?: (section: string) => void;
174
+ /** Extra class on the wrapper element. */
175
+ className?: string;
176
+ /** Extra styles on the wrapper element (`position` is reserved). */
177
+ style?: CSSProperties;
178
+ }
179
+ /**
180
+ * The element notes live in. Wrap your page's content โ€” inside whatever
181
+ * scroll container you already have โ€” and notes drop into it, ride along
182
+ * with its scroll, and measure their coordinates against it. Pages that
183
+ * scroll the window and have no sub-scopes don't need this at all โ€” the
184
+ * layer works out of the box. Reach for it to keep notes inside a
185
+ * scrollable pane, change how x is anchored, or split a page into
186
+ * review-navigable sections.
187
+ */
188
+ declare function FeedbackRegion({ children, anchorX, section, onReveal, className, style }: FeedbackRegionProps): react.JSX.Element;
189
+
190
+ export { type AnchorX, EMPTY_DOC, type EdgeTarget, type EndpointOptions, type FeedbackDoc, type FeedbackEdge, FeedbackLayer, type FeedbackNavigation, type FeedbackNote, FeedbackProvider, type FeedbackProviderProps, FeedbackRegion, type FeedbackRegionProps, type FeedbackSubmitter, type Port, type Region, endpointSubmitter, normalizeDoc };