@nexbasira/react 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 +21 -0
- package/README.md +113 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +126 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NexBasira
|
|
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,113 @@
|
|
|
1
|
+
# @nexbasira/react
|
|
2
|
+
|
|
3
|
+
React component + hook wrapping the [`@nexbasira/embed`](../embed) browser widget.
|
|
4
|
+
|
|
5
|
+
Thin by design — the iframe and postMessage plumbing live in `@nexbasira/embed`; this package just makes the widget feel native to a React app (mount, unmount, ref-based imperative access, fresh-closure callbacks).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @nexbasira/react @nexbasira/embed react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`react` and `@nexbasira/embed` are peer dependencies — they aren't bundled.
|
|
14
|
+
|
|
15
|
+
## Quick start (component)
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { NexBasiraSession } from "@nexbasira/react";
|
|
19
|
+
|
|
20
|
+
function InspectionPage({ joinUrl }: { joinUrl: string }) {
|
|
21
|
+
return (
|
|
22
|
+
<NexBasiraSession
|
|
23
|
+
sessionUrl={joinUrl}
|
|
24
|
+
height="720px"
|
|
25
|
+
onSessionComplete={(id) => {
|
|
26
|
+
console.log("inspection done:", id);
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`sessionUrl` is the field-join URL your backend mints via `POST /v1/public/sessions/{id}/invites` (or the SPA equivalent). Never embed an API secret in the URL.
|
|
34
|
+
|
|
35
|
+
## Imperative control (ref)
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useRef } from "react";
|
|
39
|
+
import { NexBasiraSession, type NexBasiraSessionHandle } from "@nexbasira/react";
|
|
40
|
+
|
|
41
|
+
function ToolbarHost({ joinUrl }: { joinUrl: string }) {
|
|
42
|
+
const session = useRef<NexBasiraSessionHandle>(null);
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<button onClick={() => session.current?.requestSnapshot()}>
|
|
46
|
+
Capture
|
|
47
|
+
</button>
|
|
48
|
+
<NexBasiraSession ref={session} sessionUrl={joinUrl} />
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Methods on the handle:
|
|
55
|
+
|
|
56
|
+
- `requestSnapshot()`
|
|
57
|
+
- `openWhiteboard()` / `closeWhiteboard()`
|
|
58
|
+
- `switchCamera()`
|
|
59
|
+
- `mute()` / `unmute()`
|
|
60
|
+
- `endSession()`
|
|
61
|
+
|
|
62
|
+
## Hook variant
|
|
63
|
+
|
|
64
|
+
Use this when `<NexBasiraSession>`'s wrapper `<div>` doesn't fit your layout, or when you want to share the widget handle across multiple components without prop-drilling refs.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useNexBasiraSession } from "@nexbasira/react";
|
|
68
|
+
|
|
69
|
+
function CustomLayout({ joinUrl }: { joinUrl: string }) {
|
|
70
|
+
const [containerRef, widget] = useNexBasiraSession({
|
|
71
|
+
sessionUrl: joinUrl,
|
|
72
|
+
onSessionComplete: (id) => navigate(`/inspections/${id}`),
|
|
73
|
+
});
|
|
74
|
+
return (
|
|
75
|
+
<div className="my-layout">
|
|
76
|
+
<header>
|
|
77
|
+
<button onClick={() => widget.current?.requestSnapshot()}>
|
|
78
|
+
Snap
|
|
79
|
+
</button>
|
|
80
|
+
</header>
|
|
81
|
+
<div ref={containerRef} style={{ flex: 1 }} />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Lifecycle callbacks
|
|
88
|
+
|
|
89
|
+
Every callback supported by `@nexbasira/embed` is passed through unchanged:
|
|
90
|
+
|
|
91
|
+
| Prop | Fires when |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `onReady` | The iframe finished loading + handshake completed. |
|
|
94
|
+
| `onSessionJoined(id)` | The field user joined the session. |
|
|
95
|
+
| `onSessionComplete(id)` | The session was closed (final state, recording paths queued). |
|
|
96
|
+
| `onEvidenceAdded(ev)` | A new evidence row was created (snapshot, whiteboard, etc.). |
|
|
97
|
+
| `onWhiteboardOpened` / `onWhiteboardSaved(ev)` | Whiteboard lifecycle. |
|
|
98
|
+
| `onParticipantJoined(p)` / `onParticipantLeft(p)` | Participant changes. |
|
|
99
|
+
| `onError({message, code})` | Anything the iframe surfaces as an error. |
|
|
100
|
+
|
|
101
|
+
Callbacks are always re-read from the latest props on every re-render — closing over fresh state in your handlers works without remounting the iframe.
|
|
102
|
+
|
|
103
|
+
## When the iframe rebuilds
|
|
104
|
+
|
|
105
|
+
The iframe is re-created **only** when `sessionUrl` changes. Every other prop (callbacks, `width`, `height`, `expectedOrigin`) is applied without remounting, so toolbars and overlays stay continuous through a session.
|
|
106
|
+
|
|
107
|
+
## TypeScript
|
|
108
|
+
|
|
109
|
+
`NexBasiraSessionProps`, `NexBasiraSessionHandle`, `EmbedOptions`, and `EmbedWidget` are all exported. The component is a `forwardRef`, so `useRef<NexBasiraSessionHandle>()` gives you typed handle access.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EmbedOptions, EmbedWidget } from "@nexbasira/embed";
|
|
2
|
+
import { type CSSProperties, type Ref } from "react";
|
|
3
|
+
/** Props for `<NexBasiraSession>` — superset of `EmbedOptions`
|
|
4
|
+
* minus `container` (the component manages its own div). Adds a
|
|
5
|
+
* `className` + `style` so callers can position the wrapper. */
|
|
6
|
+
export type NexBasiraSessionProps = Omit<EmbedOptions, "container"> & {
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
};
|
|
10
|
+
/** Imperative handle exposed via `ref` — mirrors the methods on
|
|
11
|
+
* `EmbedWidget`, so callers get the same shape they'd get from
|
|
12
|
+
* `@nexbasira/embed` directly. */
|
|
13
|
+
export interface NexBasiraSessionHandle {
|
|
14
|
+
requestSnapshot(): void;
|
|
15
|
+
openWhiteboard(): void;
|
|
16
|
+
closeWhiteboard(): void;
|
|
17
|
+
switchCamera(): void;
|
|
18
|
+
mute(): void;
|
|
19
|
+
unmute(): void;
|
|
20
|
+
endSession(): void;
|
|
21
|
+
/** Phase 1B-I — recording lifecycle from the host page. Routes
|
|
22
|
+
* through to the iframe's RecordingControl. */
|
|
23
|
+
startRecording(): void;
|
|
24
|
+
stopRecording(): void;
|
|
25
|
+
pauseRecording(): void;
|
|
26
|
+
resumeRecording(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare const NexBasiraSession: import("react").ForwardRefExoticComponent<Omit<EmbedOptions, "container"> & {
|
|
29
|
+
className?: string;
|
|
30
|
+
style?: CSSProperties;
|
|
31
|
+
} & import("react").RefAttributes<NexBasiraSessionHandle>>;
|
|
32
|
+
/** Hook variant. Returns `[containerRef, widget]`. Attach
|
|
33
|
+
* `containerRef` to the host element (typically a `<div>`), and
|
|
34
|
+
* call methods on `widget.current` once the component is mounted.
|
|
35
|
+
*
|
|
36
|
+
* Use this when `<NexBasiraSession>`'s built-in wrapper div
|
|
37
|
+
* doesn't fit your layout, or when you want fine-grained control
|
|
38
|
+
* over when the embed widget initialises. */
|
|
39
|
+
export declare function useNexBasiraSession(options: Omit<EmbedOptions, "container">): [Ref<HTMLDivElement>, {
|
|
40
|
+
current: EmbedWidget | null;
|
|
41
|
+
}];
|
|
42
|
+
export type { EmbedOptions, EmbedWidget } from "@nexbasira/embed";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @nexbasira/react — React wrapper around `@nexbasira/embed`.
|
|
4
|
+
*
|
|
5
|
+
* Two surfaces:
|
|
6
|
+
*
|
|
7
|
+
* 1. `<NexBasiraSession>` — drop-in component for the common
|
|
8
|
+
* case. Render it with `sessionUrl` and the lifecycle props
|
|
9
|
+
* you care about; the iframe lives inside, the widget gets
|
|
10
|
+
* created on mount, the listener is torn down on unmount.
|
|
11
|
+
*
|
|
12
|
+
* 2. `useNexBasiraSession()` — hook for callers that need
|
|
13
|
+
* imperative access (forwarding camera commands from a
|
|
14
|
+
* separate toolbar, hooking into an existing layout). Returns
|
|
15
|
+
* both the container ref to attach + the widget handle so
|
|
16
|
+
* you can call `.requestSnapshot()` etc. from anywhere.
|
|
17
|
+
*
|
|
18
|
+
* The wrapper is intentionally a leaf — it doesn't restyle the
|
|
19
|
+
* iframe or impose layout decisions, and lifecycle callbacks pass
|
|
20
|
+
* through unchanged so existing `@nexbasira/embed` consumers can
|
|
21
|
+
* migrate without rewiring handler signatures.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* import { NexBasiraSession } from "@nexbasira/react";
|
|
25
|
+
*
|
|
26
|
+
* <NexBasiraSession
|
|
27
|
+
* sessionUrl={mintedFieldJoinUrl}
|
|
28
|
+
* onSessionComplete={(id) => router.push(`/inspections/${id}`)}
|
|
29
|
+
* height="720px"
|
|
30
|
+
* />
|
|
31
|
+
*/
|
|
32
|
+
import { embed } from "@nexbasira/embed";
|
|
33
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, } from "react";
|
|
34
|
+
export const NexBasiraSession = forwardRef(function NexBasiraSession(props, ref) {
|
|
35
|
+
const { className, style, ...embedProps } = props;
|
|
36
|
+
const containerRef = useRef(null);
|
|
37
|
+
const widgetRef = useRef(null);
|
|
38
|
+
// Stash the latest props in a ref so the embed widget's
|
|
39
|
+
// lifecycle callbacks always read fresh closures — without
|
|
40
|
+
// this, a parent that re-renders with a new onSessionComplete
|
|
41
|
+
// (e.g. closing over fresh state) would see the stale one
|
|
42
|
+
// because the embed widget was created with the first mount's
|
|
43
|
+
// props.
|
|
44
|
+
const propsRef = useRef(embedProps);
|
|
45
|
+
propsRef.current = embedProps;
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!containerRef.current)
|
|
48
|
+
return;
|
|
49
|
+
const widget = embed({
|
|
50
|
+
...propsRef.current,
|
|
51
|
+
container: containerRef.current,
|
|
52
|
+
// Wrap each callback so the embed widget always invokes the
|
|
53
|
+
// current prop value, not the snapshot from the mount.
|
|
54
|
+
onReady: () => propsRef.current.onReady?.(),
|
|
55
|
+
onSessionJoined: (id) => propsRef.current.onSessionJoined?.(id),
|
|
56
|
+
onSessionComplete: (id) => propsRef.current.onSessionComplete?.(id),
|
|
57
|
+
onEvidenceAdded: (ev) => propsRef.current.onEvidenceAdded?.(ev),
|
|
58
|
+
onWhiteboardOpened: () => propsRef.current.onWhiteboardOpened?.(),
|
|
59
|
+
onWhiteboardSaved: (ev) => propsRef.current.onWhiteboardSaved?.(ev),
|
|
60
|
+
onParticipantJoined: (p) => propsRef.current.onParticipantJoined?.(p),
|
|
61
|
+
onParticipantLeft: (p) => propsRef.current.onParticipantLeft?.(p),
|
|
62
|
+
onError: (err) => propsRef.current.onError?.(err),
|
|
63
|
+
});
|
|
64
|
+
widgetRef.current = widget;
|
|
65
|
+
return () => {
|
|
66
|
+
widget.destroy();
|
|
67
|
+
widgetRef.current = null;
|
|
68
|
+
};
|
|
69
|
+
// Intentionally re-create the iframe only when sessionUrl
|
|
70
|
+
// changes — every other prop is read through propsRef. A
|
|
71
|
+
// sessionUrl change means a different session; the iframe
|
|
72
|
+
// must reload.
|
|
73
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
+
}, [embedProps.sessionUrl]);
|
|
75
|
+
useImperativeHandle(ref, () => ({
|
|
76
|
+
requestSnapshot: () => widgetRef.current?.requestSnapshot(),
|
|
77
|
+
openWhiteboard: () => widgetRef.current?.openWhiteboard(),
|
|
78
|
+
closeWhiteboard: () => widgetRef.current?.closeWhiteboard(),
|
|
79
|
+
switchCamera: () => widgetRef.current?.switchCamera(),
|
|
80
|
+
mute: () => widgetRef.current?.mute(),
|
|
81
|
+
unmute: () => widgetRef.current?.unmute(),
|
|
82
|
+
endSession: () => widgetRef.current?.endSession(),
|
|
83
|
+
startRecording: () => widgetRef.current?.startRecording(),
|
|
84
|
+
stopRecording: () => widgetRef.current?.stopRecording(),
|
|
85
|
+
pauseRecording: () => widgetRef.current?.pauseRecording(),
|
|
86
|
+
resumeRecording: () => widgetRef.current?.resumeRecording(),
|
|
87
|
+
}), []);
|
|
88
|
+
return _jsx("div", { ref: containerRef, className: className, style: style });
|
|
89
|
+
});
|
|
90
|
+
/** Hook variant. Returns `[containerRef, widget]`. Attach
|
|
91
|
+
* `containerRef` to the host element (typically a `<div>`), and
|
|
92
|
+
* call methods on `widget.current` once the component is mounted.
|
|
93
|
+
*
|
|
94
|
+
* Use this when `<NexBasiraSession>`'s built-in wrapper div
|
|
95
|
+
* doesn't fit your layout, or when you want fine-grained control
|
|
96
|
+
* over when the embed widget initialises. */
|
|
97
|
+
export function useNexBasiraSession(options) {
|
|
98
|
+
const containerRef = useRef(null);
|
|
99
|
+
const widgetRef = useRef(null);
|
|
100
|
+
const propsRef = useRef(options);
|
|
101
|
+
propsRef.current = options;
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!containerRef.current)
|
|
104
|
+
return;
|
|
105
|
+
const widget = embed({
|
|
106
|
+
...propsRef.current,
|
|
107
|
+
container: containerRef.current,
|
|
108
|
+
onReady: () => propsRef.current.onReady?.(),
|
|
109
|
+
onSessionJoined: (id) => propsRef.current.onSessionJoined?.(id),
|
|
110
|
+
onSessionComplete: (id) => propsRef.current.onSessionComplete?.(id),
|
|
111
|
+
onEvidenceAdded: (ev) => propsRef.current.onEvidenceAdded?.(ev),
|
|
112
|
+
onWhiteboardOpened: () => propsRef.current.onWhiteboardOpened?.(),
|
|
113
|
+
onWhiteboardSaved: (ev) => propsRef.current.onWhiteboardSaved?.(ev),
|
|
114
|
+
onParticipantJoined: (p) => propsRef.current.onParticipantJoined?.(p),
|
|
115
|
+
onParticipantLeft: (p) => propsRef.current.onParticipantLeft?.(p),
|
|
116
|
+
onError: (err) => propsRef.current.onError?.(err),
|
|
117
|
+
});
|
|
118
|
+
widgetRef.current = widget;
|
|
119
|
+
return () => {
|
|
120
|
+
widget.destroy();
|
|
121
|
+
widgetRef.current = null;
|
|
122
|
+
};
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
}, [options.sessionUrl]);
|
|
125
|
+
return [containerRef, widgetRef];
|
|
126
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexbasira/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React component + hook wrapping the NexBasira embed widget.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/codelounge-io/certivisiopro_backend.git",
|
|
27
|
+
"directory": "sdks/react"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/codelounge-io/certivisiopro_backend/tree/main/sdks/react",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/codelounge-io/certivisiopro_backend/issues"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"nexbasira",
|
|
35
|
+
"remote-inspection",
|
|
36
|
+
"react",
|
|
37
|
+
"component",
|
|
38
|
+
"embed",
|
|
39
|
+
"iframe"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": ">=17.0.0",
|
|
46
|
+
"@nexbasira/embed": "^0.1.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@nexbasira/embed": "file:../embed",
|
|
50
|
+
"@testing-library/react": "^15.0.0",
|
|
51
|
+
"@types/react": "^18.2.0",
|
|
52
|
+
"jsdom": "^24.0.0",
|
|
53
|
+
"react": "^18.2.0",
|
|
54
|
+
"react-dom": "^18.2.0",
|
|
55
|
+
"typescript": "^5.4.0",
|
|
56
|
+
"vitest": "^1.5.0"
|
|
57
|
+
}
|
|
58
|
+
}
|