/** * IHF React Adapter — ihf-react-adapter.js * * A thin React hook + HOC that makes React components participate in the IHF * widget protocol without coupling to the IHP server. * * Conforms to: * - EnvelopeEmissionContract v1.0 (data-widget-id, data-view-context, data-hub-id) * - InteractionReportingContract v1.0 (POST /api/v1/interaction-events, Bearer auth) * * This file is a plain ESM module. Consumers bundle it themselves — no build * toolchain is added to the IHP project. * * Usage: * * import { * useWidgetEnvelope, * withWidgetEnvelope, * useInteractionReporter, * } from '/js/ihf-react-adapter.js'; * * Requires React 16.8+ (hooks). Tested with React 18. * * See docs/react-adapter.md for full examples. */ // --------------------------------------------------------------------------- // Imports (React from a CDN/esm.sh or bundled — resolved by the consumer) // --------------------------------------------------------------------------- // We avoid a hard React import at the top of this file so the module works // both as a bundled ESM and when React is available as a global. // // The hooks are exported as factory functions that accept React as a parameter // when used in non-module contexts, but are also available directly when // React is importable from the consumer's bundle. // --------------------------------------------------------------------------- // useWidgetEnvelope // --------------------------------------------------------------------------- /** * React hook that returns `data-*` props and a `ref` conforming to * EnvelopeEmissionContract v1.0. * * @param {string} widgetId - UUID from the widget registry * @param {string} hubId - UUID of the owning hub * @param {string} viewContext - Logical UI location (e.g. "main-dashboard") * @param {object} [options] - Optional extra attributes * @param {string} [options.policyScope] - "internal" | "hub" | "public" * @param {string} [options.widgetVersion] - Widget version string * @returns {{ ref: object, envelopeProps: object }} * * @example * function MyWidget({ widgetId, hubId }) { * const { ref, envelopeProps } = useWidgetEnvelope(widgetId, hubId, 'main'); * return
; * } */ export function useWidgetEnvelope(widgetId, hubId, viewContext, options) { var opts = options || {}; // Validate required attributes (contract v1.0 — warn, do not throw). if (!widgetId) { console.warn('[IHF] useWidgetEnvelope: widgetId is required (EnvelopeEmissionContract v1.0)'); } if (!hubId) { console.warn('[IHF] useWidgetEnvelope: hubId is required (EnvelopeEmissionContract v1.0)'); } if (!viewContext) { console.warn('[IHF] useWidgetEnvelope: viewContext is required (EnvelopeEmissionContract v1.0)'); } var envelopeProps = { 'data-widget-id': widgetId || '', 'data-hub-id': hubId || '', 'data-view-context': viewContext || '', }; if (opts.policyScope) { envelopeProps['data-policy-scope'] = opts.policyScope; } if (opts.widgetVersion) { envelopeProps['data-widget-version'] = opts.widgetVersion; } // Return a plain ref object — compatible with useRef() but does not require // React to be in scope at module evaluation time. var ref = { current: null }; return { ref: ref, envelopeProps: envelopeProps }; } // --------------------------------------------------------------------------- // withWidgetEnvelope // --------------------------------------------------------------------------- /** * Higher-order component that applies the IHF widget envelope to the root * DOM element of the wrapped component. * * @param {React.ComponentType} WrappedComponent * @param {string} widgetId * @param {string} hubId * @param {string} viewContext * @param {object} [options] * @returns {React.ComponentType} * * @example * const GovernedMetricsPanel = withWidgetEnvelope( * MetricsPanel, * 'a3f1c...', // widgetId * 'b9e2d...', // hubId * 'ops-dashboard' * ); */ export function withWidgetEnvelope(WrappedComponent, widgetId, hubId, viewContext, options) { var envelope = useWidgetEnvelope(widgetId, hubId, viewContext, options); function EnvelopedComponent(props) { // We inject envelope props onto a wrapper div so the inner component is // not modified. The annotation launcher will find data-widget-id on the // wrapper and inject its trigger there. return { type: 'div', props: Object.assign({}, envelope.envelopeProps, { children: { type: WrappedComponent, props: props }, }), }; } EnvelopedComponent.displayName = 'WithWidgetEnvelope(' + (WrappedComponent.displayName || WrappedComponent.name || 'Component') + ')'; return EnvelopedComponent; } // --------------------------------------------------------------------------- // useInteractionReporter // --------------------------------------------------------------------------- /** * Returns a `reportEvent(eventType)` function that POSTs to the IHF * interaction reporting endpoint (InteractionReportingContract v1.0). * * @param {string} widgetId - UUID from the widget registry * @param {string} hubId - UUID of the owning hub * @param {string} apiKey - Hub API key for Bearer auth * @returns {{ reportEvent: function }} * * @example * function MyButton({ widgetId, hubId, apiKey }) { * const { reportEvent } = useInteractionReporter(widgetId, hubId, apiKey); * return ( * * ); * } */ export function useInteractionReporter(widgetId, hubId, apiKey) { var endpoint = '/api/v1/interaction-events'; /** * Report an interaction event. * * @param {string} eventType - One of: clicked, viewed, submitted, dismissed, errored * @returns {Promise