iwmi-helpdesk-widget
v1.1.0
Published
Standalone helpdesk widget for IWMI Digital Twin applications
Maintainers
Readme
iwmi-helpdesk-widget
A drop-in React component for collecting data contributions, feedback, and support requests from researchers using IWMI Digital Twin applications (Limpopo, Somali, and any new DT spun up later).
It owns the entire submission pipeline:
POST /helpdesk/submit— creates the ticket, returns presigned S3 URLs- Direct browser
PUTto S3 with per-file progress (XHR, in parallel) POST /helpdesk/{id}/files/{file_id}/confirm— server HEAD-verifies and flips status
…and presents it as a clean, themeable form with drag-and-drop file upload, light / dark mode, and four presentation surfaces (inline / dialog / drawer / floating).
Install
npm install iwmi-helpdesk-widget
# or
pnpm add iwmi-helpdesk-widget
#or
yarn add iwmi-helpdesk-widgetreact and react-dom are peer dependencies (≥ 18; React 19 also supported).
Quickstart
import { IWMIHelpdeskWidget } from 'iwmi-helpdesk-widget'
import 'iwmi-helpdesk-widget/style.css'
export function HelpPage() {
return (
<IWMIHelpdeskWidget
apiBaseUrl="https://api.digitaltwins.iwmi.org"
dtSource="limpopo"
requester={{ name: user.fullName, email: user.email }}
/>
)
}That renders the full form inline. To open it from a button instead, set variant:
<IWMIHelpdeskWidget {...} variant="dialog" trigger="Open helpdesk" />
<IWMIHelpdeskWidget {...} variant="drawer" side="right" />
<IWMIHelpdeskWidget {...} variant="floating" position="bottom-right" />Configuration reference
Required
| Prop | Type | Notes |
| ------------ | ------------------- | --------------------------------------------------------------- |
| apiBaseUrl | string | Base URL of the DT helpdesk API. No trailing slash needed. |
| dtSource | string | Which Digital Twin this submission belongs to. Open enum — pass any value the backend accepts. Known values: 'somali', 'limpopo'. |
Identity (optional, recommended)
| Prop | Type | Notes |
| ------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------- |
| requester.name | string | Pre-fills the Full name field. |
| requester.email | string | Pre-fills the Email address field. |
| requester.locked | boolean (default true when prefilled)| Lock prefilled fields to read-only. Set false for prefilled-but-editable. |
Each field locks independently. Pass only email and the name field stays editable.
Form configuration
| Prop | Type | Notes |
| -------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| requestTypes | Array<string \| { value: string; label: string }> | Which request types to show. Strings use a default or humanised label; objects let you set custom names. |
Default: ['data_contribution', 'feedback', 'support'].
requestTypes={[
'data_contribution',
'feedback',
{ value: 'bug_report', label: 'Report a bug' },
{ value: 'access_request', label: 'Request data access' },
]}Adding a new request type does not require a widget release. Pass any string the backend accepts.
Presentation
| Prop | Type | Default | Notes |
| ------------- | ------------------------------------------------------------------------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| variant | 'inline' \| 'dialog' \| 'drawer' \| 'floating' | 'inline' | How the widget surfaces. |
| side | 'left' \| 'right' | 'right' | Which edge a drawer slides from. |
| position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' \| { top, right, bottom, left, transform } | 'bottom-right' | Where a floating trigger sits. Either a corner preset or arbitrary CSS offsets (numbers in px or any CSS length). |
| trigger | string \| ReactNode | 'Helpdesk' | Label or full element for the open trigger (dialog / drawer / floating). |
| open | boolean | — | Controlled open state. When provided, the widget renders no default trigger — the host owns when it shows. |
| onOpenChange| (open: boolean) => void | — | Fires on internal close (Esc, backdrop click) so a controlled host can sync state. |
| zIndex | number | 9997 | Stacking base. Backdrops use zIndex + 1. Bump if your host has chrome at higher z-index. |
Theming
| Prop | Type | Default | Notes |
| ------------- | ------------------------------------------------- | -------------- | ------------------------------------------------------------------------------ |
| theme.primaryColor | string | #1a6b4a | Brand colour. Used for buttons, focus rings, accents. |
| theme.borderRadius | string | 6px | Used by inputs, buttons, surfaces. |
| theme.fontFamily | string | inherits | Override the host font if you don't want it inherited. |
| colorScheme | 'light' \| 'dark' \| 'auto' | 'light' | 'auto' follows the OS-level prefers-color-scheme. |
Custom CSS via tokens
Every colour the widget uses is a CSS custom property (the --hd-* namespace).
You can override individual tokens by setting them on any ancestor:
.my-helpdesk-wrapper {
--hd-primary-color: #0f766e;
--hd-border-radius: 12px;
/* Surface */
--hd-bg: #ffffff;
--hd-bg-muted: #f1f5f9;
--hd-bg-subtle: #e2e8f0;
--hd-border: #cbd5e1;
/* Text */
--hd-text: #0f172a;
--hd-text-muted: #475569;
/* Inputs */
--hd-input-bg: #ffffff;
--hd-input-border: #cbd5e1;
/* Status */
--hd-error: #b42318;
--hd-success: #16a34a;
--hd-warning: #92400e;
}This makes the widget compatible with any host design system — Tailwind, Mantine, Chakra, etc. The dark-mode variant sets the same tokens to dark equivalents, so overrides automatically apply to both themes.
Lifecycle
| Prop | Type | Notes |
| ----------- | --------------------------------- | -------------------------------------------------------------------- |
| onSuccess | (helpdeskId: number) => void | Fires once after every file is uploaded and confirmed. |
| onError | (error: Error) => void | Fires if POST /submit fails or the widget renders an error state. |
Backend contract
The widget assumes a Flask service at apiBaseUrl exposing:
| Method | Path | Auth |
| ------ | ------------------------------------------- | ------ |
| POST | /helpdesk/submit | Public |
| POST | /helpdesk/{id}/files/{file_id}/confirm | Public |
Validation rules are mirrored client-side (mostly to fail fast in the UI):
requester_emailmust contain@dt_sourceandrequest_typemirror the backend's accepted values (open string)file_format ∈ TIFF | SHAPEFILE_ZIP | GEOJSON | WMS_URL | DRIVE_LINK | ZIP | OTHER- Non-URL files require
file_nameandfile_size_bytes - URL formats (
WMS_URL,DRIVE_LINK) requireexternal_url(must parse as a URL) - Per-file size cap: 1 GB
- Maximum 5 files per request, minimum 1
The widget rejects files exceeding the cap inline, before submit.
Browser support
- Chrome / Edge ≥ 100
- Firefox ≥ 100
- Safari ≥ 16
CSS uses color-mix() and prefers-color-scheme — both supported in the matrix above.
For older browsers, fallback colours are still legible (the mix just doesn't apply).
Accessibility
- Form fields have explicit
<label>associations. - Dialog/drawer use
role="dialog"+aria-modal="true", Esc to close, body scroll-lock. - Drop zone is keyboard-accessible (Enter / Space to open file picker).
- All errors announced via
role="alert"(server errors) oraria-describedby(per-field validation). - Honors
prefers-reduced-motion— open/close transitions disable.
Bundle
Built with vite in library mode. React stays external.
| Format | Size (gzipped) | | ------ | -------------- | | ESM | ~11 kB | | UMD | ~9.6 kB | | CSS | ~3.3 kB |
Type declarations are emitted to dist/index.d.ts and resolve via the package
exports map.
Development
pnpm install
pnpm dev # live demo at http://localhost:5173
pnpm build # produces dist/ for publishingThe dev harness in dev/main.tsx intercepts fetch so the full
three-phase flow runs without a real backend. The submit endpoint is mocked; PUT
uploads go to httpbin.org/put so progress events fire end-to-end.
Publishing a release
Pushing a v* tag triggers
.github/workflows/publish.yml, which:
- Verifies
package.jsonversion matches the tag (fail-fast if mismatched). - Builds the library (typecheck + bundle +
.d.ts). - Publishes to npm with the
NPM_TOKENsecret. - Extracts the matching
## [VERSION]section fromCHANGELOG.mdand creates a GitHub Release with those notes attached.
Per release:
# 1. Move CHANGELOG entries from [Unreleased] under a new [X.Y.Z] heading.
# 2. Bump "version" in package.json to match.
# 3. Commit, then:
git tag v1.2.3
git push origin main --follow-tagsThat's it — the npm package and the GitHub Release both land on tag arrival.
License
UNLICENSED — internal IWMI use.
