@ganeshdole/brainbox-react 0.0.0-stage → 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,28 @@
1
+ O'Saasy License
2
+
3
+ Copyright © 2026, Ganesh Dole.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ 1. The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ 2. No licensee or downstream recipient may use the Software (including
17
+ any modified or derivative versions) to directly compete with the
18
+ original Licensor by offering it to third parties as a hosted, managed,
19
+ or Software-as-a-Service (SaaS) product or cloud service where the
20
+ primary value of the service is the functionality of the Software itself.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
23
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
25
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
26
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
27
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
28
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,69 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @ganeshdole/brainbox-react
2
+
3
+ React bindings for `@ganeshdole/brainbox-core`: a provider that owns one instance, and
4
+ hooks that read it. The `react-redux` to core's `redux`. No UI lives here;
5
+ for Brainbox's own widget see `@ganeshdole/brainbox-widget`.
6
+
7
+ ## Install
8
+
9
+ Inside this monorepo the package is consumed from source. `react` (19+) is a
10
+ peer dependency, so a host ends up with one copy.
11
+
12
+ ```json
13
+ { "dependencies": { "@ganeshdole/brainbox-react": "workspace:*" } }
14
+ ```
15
+
16
+ Outside it, from npm:
17
+
18
+ ```sh
19
+ npm install @ganeshdole/brainbox-react @ganeshdole/brainbox-core
20
+ ```
21
+
22
+ `pnpm build` emits `dist/` (JS plus `.d.ts`); the published package's
23
+ `exports` point at it (`publishConfig`).
24
+
25
+ ## Use
26
+
27
+ ```tsx
28
+ import { BrainboxProvider, useBrainbox, useDraft } from "@ganeshdole/brainbox-react";
29
+ import { brainboxTransport } from "@ganeshdole/brainbox-core";
30
+
31
+ // Keep the transport stable: a new one means a new instance.
32
+ const transport = brainboxTransport({ endpoint, projectKey });
33
+
34
+ export function Root() {
35
+ return (
36
+ <BrainboxProvider transport={transport} identity={{ id: user.id, email: user.email }}>
37
+ <App />
38
+ </BrainboxProvider>
39
+ );
40
+ }
41
+ ```
42
+
43
+ Your own UI on top of the engine:
44
+
45
+ ```tsx
46
+ function FeedbackPanel() {
47
+ const brainbox = useBrainbox(); // null on the server and the first render
48
+ const panel = useRef<HTMLDivElement>(null);
49
+ const draft = useMemo(() => brainbox?.draft({ exclude: panel.current ?? undefined }), [brainbox]);
50
+ if (!draft) return null;
51
+ return <Panel ref={panel} draft={draft} />;
52
+ }
53
+
54
+ function Panel({ draft }: { draft: Draft }) {
55
+ const state = useDraft(draft); // re-renders on every change
56
+ return state.screenshotPending ? <Spinner /> : <img src={state.screenshotUrl ?? ""} />;
57
+ }
58
+ ```
59
+
60
+ ## How the provider behaves
61
+
62
+ - The instance is created in an effect and destroyed in the cleanup. That
63
+ keeps a server render side-effect free and leaves exactly one instance
64
+ under StrictMode's mount-unmount-mount. The cost is one render in which
65
+ `useBrainbox()` is `null`.
66
+ - `identity` changes call `identify()` on the live instance. `onSubmitted`
67
+ may change freely. Only a new `transport` rebuilds the instance.
68
+ - `<BrainboxProvider brainbox={instance}>` provides an instance you created
69
+ and own. Shells outside React (the script-tag bundle) use this.
@@ -0,0 +1,2 @@
1
+ export { BrainboxProvider, useBrainbox, type BrainboxProviderProps } from "./provider.tsx";
2
+ export { useDraft } from "./use-draft.ts";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // @ganeshdole/brainbox-react - React bindings for @ganeshdole/brainbox-core.
2
+ export { BrainboxProvider, useBrainbox } from "./provider.js";
3
+ export { useDraft } from "./use-draft.js";
@@ -0,0 +1,38 @@
1
+ import { type ReactNode } from "react";
2
+ import { type Brainbox, type Identity, type SubmitResult, type Transport } from "@ganeshdole/brainbox-core";
3
+ interface OwnedProps {
4
+ /** Where finished reports go. Keep it stable (module-level or `useMemo`):
5
+ * a new transport means a new instance. */
6
+ transport: Transport;
7
+ /** Who is filing. Changing it calls `identify()` on the live instance. */
8
+ identity?: Identity;
9
+ /** Called after every successful submit. May change between renders. */
10
+ onSubmitted?: (result: SubmitResult) => void;
11
+ brainbox?: never;
12
+ }
13
+ interface GivenProps {
14
+ /** An instance the caller created and will `destroy()` itself. For shells
15
+ * outside React, like the script-tag bundle, that need the instance before
16
+ * anything renders. */
17
+ brainbox: Brainbox;
18
+ transport?: never;
19
+ identity?: never;
20
+ onSubmitted?: never;
21
+ }
22
+ export type BrainboxProviderProps = (OwnedProps | GivenProps) & {
23
+ children?: ReactNode;
24
+ };
25
+ /**
26
+ * Makes one `Brainbox` available to the tree below it.
27
+ *
28
+ * Given `transport`, it owns the instance: created in an effect, not during
29
+ * render, because creating it patches `console.error`, which must not happen
30
+ * on the server and must happen exactly once under StrictMode's
31
+ * mount-unmount-mount. The cost is one render where `useBrainbox()` is still
32
+ * `null`. Given `brainbox`, it only provides it.
33
+ */
34
+ export declare function BrainboxProvider(props: BrainboxProviderProps): import("react").JSX.Element;
35
+ /** The nearest provider's instance. `null` during the first render and on the
36
+ * server, before the provider's effect has created it. */
37
+ export declare function useBrainbox(): Brainbox | null;
38
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
3
+ import { createBrainbox, } from "@ganeshdole/brainbox-core";
4
+ const BrainboxContext = createContext(null);
5
+ /**
6
+ * Makes one `Brainbox` available to the tree below it.
7
+ *
8
+ * Given `transport`, it owns the instance: created in an effect, not during
9
+ * render, because creating it patches `console.error`, which must not happen
10
+ * on the server and must happen exactly once under StrictMode's
11
+ * mount-unmount-mount. The cost is one render where `useBrainbox()` is still
12
+ * `null`. Given `brainbox`, it only provides it.
13
+ */
14
+ export function BrainboxProvider(props) {
15
+ if (props.brainbox) {
16
+ return (_jsx(BrainboxContext.Provider, { value: props.brainbox, children: props.children }));
17
+ }
18
+ return _jsx(OwnedProvider, { ...props });
19
+ }
20
+ function OwnedProvider({ transport, identity, onSubmitted, children, }) {
21
+ const [brainbox, setBrainbox] = useState(null);
22
+ // Read through refs so a new callback or identity does not rebuild the instance.
23
+ const onSubmittedRef = useRef(onSubmitted);
24
+ useEffect(() => {
25
+ onSubmittedRef.current = onSubmitted;
26
+ }, [onSubmitted]);
27
+ const identityRef = useRef(identity);
28
+ useEffect(() => {
29
+ identityRef.current = identity;
30
+ }, [identity]);
31
+ useEffect(() => {
32
+ const options = {
33
+ transport,
34
+ identity: identityRef.current,
35
+ onSubmitted: (result) => onSubmittedRef.current?.(result),
36
+ };
37
+ const instance = createBrainbox(options);
38
+ setBrainbox(instance);
39
+ return () => {
40
+ instance.destroy();
41
+ setBrainbox(null);
42
+ };
43
+ }, [transport]);
44
+ useEffect(() => {
45
+ if (brainbox && identity)
46
+ brainbox.identify(identity);
47
+ }, [brainbox, identity]);
48
+ return _jsx(BrainboxContext.Provider, { value: brainbox, children: children });
49
+ }
50
+ /** The nearest provider's instance. `null` during the first render and on the
51
+ * server, before the provider's effect has created it. */
52
+ export function useBrainbox() {
53
+ return useContext(BrainboxContext);
54
+ }
@@ -0,0 +1,3 @@
1
+ import type { Draft, DraftState } from "@ganeshdole/brainbox-core";
2
+ /** A draft's state, re-rendering the component whenever it changes. */
3
+ export declare function useDraft(draft: Draft): DraftState;
@@ -0,0 +1,5 @@
1
+ import { useSyncExternalStore } from "react";
2
+ /** A draft's state, re-rendering the component whenever it changes. */
3
+ export function useDraft(draft) {
4
+ return useSyncExternalStore(draft.subscribe, draft.getState, draft.getState);
5
+ }
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@ganeshdole/brainbox-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "React bindings for @ganeshdole/brainbox-core: a provider that owns one Brainbox instance and hooks that read it. No UI.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "homepage": "https://www.brainbox.sh",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ganeshdole/brainbox.git",
10
+ "directory": "packages/react"
11
+ },
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist",
24
+ "README.md"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@ganeshdole/brainbox-core": "0.1.0"
31
+ },
32
+ "peerDependencies": {
33
+ "react": "^19.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@eslint/js": "^10.0.1",
37
+ "@types/react": "^19.2.16",
38
+ "@types/react-dom": "^19.2.3",
39
+ "eslint": "^10.4.1",
40
+ "eslint-plugin-react-hooks": "^7.1.1",
41
+ "globals": "^17.6.0",
42
+ "jsdom": "^26.1.0",
43
+ "react": "^19.2.7",
44
+ "react-dom": "^19.2.7",
45
+ "typescript": "^6.0.3",
46
+ "typescript-eslint": "^8.60.1",
47
+ "vitest": "^3.2.4"
48
+ },
49
+ "scripts": {
50
+ "lint": "eslint .",
51
+ "build": "tsc -p tsconfig.build.json",
52
+ "typecheck": "tsc -p tsconfig.json",
53
+ "test": "vitest run"
54
+ }
6
55
  }