feat(P6/T06): React adapter specification and reference example

ihf-react-adapter.js: useWidgetEnvelope (EnvelopeEmissionContract v1.0 props),
withWidgetEnvelope HOC, useInteractionReporter (POSTs to reporting contract endpoint).
Plain ESM module — no IHP build toolchain required. docs/react-adapter.md with
usage examples. static/ihf-react-test.html: React widget + native IHP widget on
same page demonstrating annotation launcher pickup and reportEvent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:19:26 +00:00
parent 04eb4643b0
commit 803376b09a
4 changed files with 472 additions and 1 deletions

132
docs/react-adapter.md Normal file
View File

@@ -0,0 +1,132 @@
# IHF React Adapter
`static/js/ihf-react-adapter.js` provides three exports that let React
components participate in the IHF widget protocol:
| Export | What it does |
|------------------------|---------------------------------------------------------|
| `useWidgetEnvelope` | Hook — returns `data-*` props for the envelope |
| `withWidgetEnvelope` | HOC — wraps a component with the envelope div |
| `useInteractionReporter` | Hook — returns `reportEvent(type)` for API submission |
The adapter conforms to:
- **EnvelopeEmissionContract v1.0** — emits `data-widget-id`, `data-view-context`, `data-hub-id`
- **InteractionReportingContract v1.0** — POSTs to `/api/v1/interaction-events` with Bearer auth
No build toolchain is added to the IHP project. Consumers bundle this file
themselves (webpack, vite, esbuild, etc.) or load it via `<script type="module">`.
---
## useWidgetEnvelope
```jsx
import { useWidgetEnvelope } from '/js/ihf-react-adapter.js';
function MetricsPanel({ widgetId, hubId }) {
const { ref, envelopeProps } = useWidgetEnvelope(
widgetId,
hubId,
'ops-dashboard', // viewContext
{ policyScope: 'internal' } // optional
);
return (
<div ref={ref} {...envelopeProps} className="p-4 border rounded">
<h2>Metrics</h2>
{/* content */}
</div>
);
}
```
The `envelopeProps` object contains all required `data-*` attributes.
The `ref` allows the annotation launcher to find the element.
---
## withWidgetEnvelope
```jsx
import { withWidgetEnvelope } from '/js/ihf-react-adapter.js';
// Wrap at module definition time, supplying stable IDs:
const GovernedMetricsPanel = withWidgetEnvelope(
MetricsPanel,
'a3f1c2b4-...', // widgetId (UUID from widget registry)
'b9e2d3a1-...', // hubId
'ops-dashboard' // viewContext
);
// Use anywhere:
function Dashboard() {
return <GovernedMetricsPanel title="CPU Usage" />;
}
```
The HOC wraps the component in an envelope `<div>` carrying the `data-*`
attributes. The inner component receives its props unchanged.
---
## useInteractionReporter
```jsx
import { useInteractionReporter } from '/js/ihf-react-adapter.js';
function ActionButton({ widgetId, hubId, apiKey }) {
const { reportEvent } = useInteractionReporter(widgetId, hubId, apiKey);
async function handleClick() {
await reportEvent('clicked');
// proceed with action
}
return <button onClick={handleClick}>Run Report</button>;
}
```
Accepted event types (InteractionReportingContract v1.0):
`clicked`, `viewed`, `submitted`, `dismissed`, `errored`
The `apiKey` is the hub's bearer token (`hubs.api_key`). Retrieve it from
your app's server-side config — do not embed it in public bundle output.
---
## Combined example
```jsx
import {
useWidgetEnvelope,
useInteractionReporter,
} from '/js/ihf-react-adapter.js';
function GovernedButton({ widgetId, hubId, apiKey, label, onAction }) {
const { envelopeProps } = useWidgetEnvelope(widgetId, hubId, 'action-bar');
const { reportEvent } = useInteractionReporter(widgetId, hubId, apiKey);
async function handleClick() {
await reportEvent('clicked');
onAction();
}
return (
<div {...envelopeProps}>
<button onClick={handleClick}>{label}</button>
</div>
);
}
```
The annotation launcher (`ihf-annotation-launcher.js`) will automatically
detect the `data-widget-id` attribute and inject an Annotate trigger alongside
this component if `IHP_ANNOTATION_LAUNCHER=true`.
---
## Test fixture
`static/ihf-react-test.html` demonstrates a React widget alongside an
IHP-rendered widget on the same page. Open it directly in the browser
(no server needed for the React side).