@botiverse/hands-feedback-react
v0.4.1
Published
Embeddable reporter-facing Hands feedback inbox components.
Keywords
Readme
@botiverse/hands-feedback-react
Reporter-facing Hands feedback components. The package renders a ticket inbox,
conversation detail, reply composer, and new-feedback form with elegant and
brutal themes.
import {
FeedbackProvider,
FeedbackWorkspace,
type HandsFeedbackTransport,
} from "@botiverse/hands-feedback-react";
import "raft-ui/styles.css";
import "@botiverse/hands-feedback-react/styles.css";
// Implement this in your app. It calls your authenticated same-origin proxy;
// the complete adapter is in the integration guide linked below.
const transport: HandsFeedbackTransport =
createMyAppFeedbackTransport("/api/feedback");
<FeedbackProvider
transport={transport}
theme="brutal"
// Optional. Without this override the SDK maps browser `zh*` to `zh-CN`
// and falls back to English. Pass the host app's selected locale when set.
locale="en"
onUnreadChanged={({ total }) => setFeedbackBadge(total)}
>
<FeedbackWorkspace />
</FeedbackProvider>;The workspace owns the reporter interaction state: list filter/scroll/focus restoration, per-ticket reply drafts, IME-safe Enter handling, attachment progress/retry/cancel, recoverable cursor pages, and near-bottom conversation following. Hosts should not duplicate that state machine. They provide only the reporter-scoped transport, route notifications, and (optionally) an attachment opener.
On mobile, hosts can bind those workspace notifications to real browser URLs and enable the package's touch-only pull-to-refresh behavior:
<FeedbackWorkspace
route={routeFromLocation(location)}
onRouteChange={(nextRoute, options) =>
navigate(pathForRoute(nextRoute), { replace: options?.replace })
}
enablePullToRefresh={isMobile}
/>inbox, new, and ticket are the supported workspace routes. The SDK marks
the create-success transition from new to ticket with replace: true, so
Back does not reopen a submitted form. Pull-to-refresh is attached only to the
list and conversation scroll viewports, starts only at scroll-top, ignores
editable controls, and preserves existing content while authoritative data is
reloaded. The generic usePullToRefresh hook is also exported for other host
scroll surfaces.
All visible copy, closed error copy, dates, and accessibility labels share the
SDK locale. Supported locales are en and zh-CN; an explicit provider
locale takes precedence over browser language detection.
Localization is provider-driven rather than limited to the built-in bundles.
messages accepts a typed partial override (missing keys fall back to the
resolved locale), including parameterized validation strings. formatDate
and formatFileSize let the host apply its own formatting conventions without
rebuilding SDK UI. Browser negotiation selects the first supported entry in
navigator.languages order, then falls back to English.
<FeedbackProvider
transport={transport}
locale={appLocale}
messages={{
newFeedback: t("feedback.new"),
attachmentUnsupported: t("feedback.attachmentUnsupported"), // `{name}`
}}
formatDate={(date, { locale }) => appDateFormatter(date, locale)}
formatFileSize={(bytes, { locale }) => appFileSizeFormatter(bytes, locale)}
>
<FeedbackWorkspace />
</FeedbackProvider>Host-staged attachments
A host may put one small file (for example a validated diagnostic snapshot)
into the reporter's pending attachments. Prepare the File first (any async
fetch/validation happens before this click), then call the handle
synchronously from the click handler:
const workspace = useRef<FeedbackWorkspaceHostHandle>(null);
<button
onClick={() => {
// No `await` before this call: it needs the live user gesture.
const result = workspace.current?.openNewFeedbackWithPendingFile({
file: preparedDiagnosticFile,
});
if (result && !result.ok) showReason(result.reason);
}}
>
Report issue with diagnostic
</button>
<FeedbackWorkspace
ref={workspace}
onOpenPendingAttachment={({ file }) => previewInHost(file)}
/>openNewFeedbackWithPendingFile({ file })opens the new-feedback form with the file already pending in the same call. If the form is already open, it adds the file there. It is synchronous and returns a typed result.attachPendingFile({ file })is the low-level variant: it only stages into an already open form and returnscomposer_closedotherwise.
The contract (both methods):
- Stage only. They never upload or submit. The reporter sees the file,
can open it (via
onOpenPendingAttachment, including non-image files), can remove it, and it is sent only when they press Submit. - User gesture required. Without transient browser user activation
(
navigator.userActivation.isActive) they returnuser_activation_requiredand change nothing, including the route. - Bytes only. Input is exactly
{ file: File }. No paths, URLs, callbacks, or background fetches. - Bounded. At most
MAX_FEEDBACK_HOST_ATTACHMENTS(1) host file,MAX_FEEDBACK_HOST_ATTACHMENT_BYTES(1 MiB), types inFEEDBACK_HOST_ATTACHMENT_TYPES(the screenshot image types plusapplication/jsonandtext/plain), and it counts toward the shared 3-attachment limit. The same file (name, type, size, lastModified) is rejected asduplicate. - Typed rejection, zero mutation. Failures return
{ ok: false, reason }(FeedbackHostAttachmentRejection) and do not navigate, change the pending list, show an error banner, or upload anything.busymeans a submission is in flight.
Your Hands app and transport must accept the injected MIME type. The Hands public submit endpoint accepts any type. Reporter replies accept only images, which is why host files go to the new-feedback form only.
Security boundary
- The package has no
appToken,clientSecret,reporterId, or arbitrary owner prop. - The host transport binds a short-lived reporter-scoped session.
- Hands is authoritative for tickets and read/unread state.
unreadTotalcomes from Hands responses; the component never derives or persists its own read cursor. - A detail request reports and clears unread only after its successful Hands response. Failed, aborted, stale, and unmounted reads do not mutate unread.
- Webhooks are optional server-to-server integrations and are not required for inbox correctness.
Consumers using Tailwind v4 should include the compiled package as a source if their build purges library classes:
@source '../node_modules/raft-ui/dist';
@source '../node_modules/@botiverse/hands-feedback-react/dist';For a source-pinned integration before an npm release, install this package's
Hands monorepo subdirectory with pnpm's path: git selector, then import the
explicit source exports:
pnpm add "github:botiverse/hands#<full-commit-sha>&path:packages/feedback-react"
pnpm add react react-dom raft-ui lucide-reactimport {
FeedbackProvider,
FeedbackWorkspace,
} from "@botiverse/hands-feedback-react/source";
import "@botiverse/hands-feedback-react/source/styles.css";The host bundler must support TypeScript/TSX dependencies. Published consumers should use the compiled root exports shown above.
The complete server credential, opaque reporter identity, route binding, proxy, transport, webhook, and production-checklist walkthrough is in the React Feedback Inbox guide.
