@casecore/support-widget
v0.7.0
Published
Embeddable CaseCore support ticket form for React applications
Readme
@casecore/support-widget
React 18/19 support form. It only calls an authenticated same-origin BFF; the CaseCore credential must stay in the host application's backend.
Installation
pnpm add @casecore/support-widgetThe package is public and installs from npm without a registry token. Never place a CaseCore intake credential in browser environment variables.
import { SupportWidget } from '@casecore/support-widget';
<SupportWidget
endpoint="/api/support/tickets"
locale="ru"
defaultContact={{ email: session.user.email, preferredChannel: 'email' }}
request={authenticatedProductFetch}
/>;The endpoint must be a same-origin path. The host BFF authenticates the current user and maps the
widget payload to @casecore/intake-client. A host that uses bearer-token sessions can provide
the narrow request(endpoint, init) adapter shown above. That adapter may add only the product
session; a CaseCore intake credential must never be attached in browser code.
When the BFF response includes data.portalUrl, the success state renders an “open ticket” link.
The widget never constructs the URL itself and keeps the case-number-only success state when the
field is absent. CaseCore Intake returns an absolute path such as /portal/case/...; when the BFF
and CaseCore Web use different public origins, the BFF must resolve that path against the
configured public CaseCore Web origin before returning it to the widget. Do this server-side and
do not log the resulting URL because it contains the reporter's bearer token.
Contact fields
defaultContact?: { email?: string; phone?: string; preferredChannel?: 'email' | 'phone' }
pre-fills the Email/Phone inputs and preferred-contact-method choice. The user can still edit or
clear any of it before submit — whatever is currently in the form is what gets sent, the default
is never silently re-merged server-side.
Attachments
The dialog accepts files via a visible file picker, drag-and-drop onto the dialog, and pasting an
image from the clipboard (Ctrl+V). Selected files get a name/size preview with a per-file remove
control. Client-side validation uses the package-owned supportAttachmentLimits export
and blocks submit with an inline error before any network call. This is a UX convenience only;
the CaseCore API performs the authoritative check.
When at least one file is attached, the widget POSTs multipart/form-data to endpoint with a
payload field (the same JSON body sent when there are no attachments, as a string) and one
files part per attachment, so the host app's BFF can forward each file to
intake-client.uploadAttachment() and pass the resulting tokens as attachments on
createTicket(). With no attachments, the request body is unchanged (application/json).
Host styling
The dialog carries no inline styles. It ships one stylesheet of single-class selectors
(support-form__*) reading CSS custom properties, injected into document.head on first
open. Set any of the tokens below anywhere in the host's own CSS — they are declared inside
:where(:root), which has zero specificity, so a plain :root { --support-accent: … } wins without
!important:
:root {
--support-accent: #0e7ab0;
--support-accent-ink: #ffffff;
--support-radius: 4px;
--support-font: 'Inter', sans-serif;
}The full list is exported as supportThemeTokens. A dark palette is applied under
prefers-color-scheme: dark, and at 640px and below the dialog becomes a bottom sheet with a
pinned header and action bar.
A host that owns the look entirely passes injectStyles={false} and ships its own rules against
the same class names; supportFormStyles exports the default sheet if it wants to start
from it. trigger, triggerClassName, and triggerStyle still control the opening button, and
contextLabel puts a short "where this came from" chip in the footer.
The dialog is portaled to document.body, so transformed, clipped, or narrow host containers
cannot constrain its fixed backdrop. It remains owned by CaseCore so responsive layout, focus
handling, validation, locale copy, accessibility behavior, the self-service portal link, and the
BFF contract change in one package.
Building your own form on the same contract
A product whose design system will not accept a foreign dialog imports the headless layer instead of the widget and renders its own markup. Validation, the attachment limits, the idempotency key, the multipart assembly and the BFF contract stay in this package:
import { useSupportForm } from '@casecore/support-widget';
const form = useSupportForm({ endpoint: '/api/support/tickets', context });
<input value={form.title} onChange={(event) => form.setTitle(event.target.value)} />;
{
form.titleError ? <p>{form.titleError}</p> : null;
}
<button onClick={() => void form.submit()} disabled={form.state === 'submitting'}>
Send
</button>;form.state moves through idle → submitting → success | error. After an error the draft is
kept: form.submit() retries under the same idempotency key, so a send that actually reached
CaseCore before the connection dropped returns the original case instead of filing a second one.
form.reset() clears the draft and takes a new key for the next delivery.
Несовместимые изменения в 0.7.0
Имена, которые виджет оставлял в чужом приложении, больше не содержат названия системы поддержки — оно описывает CaseCore, а не функцию, которую встраивает продукт:
| Было | Стало |
| --------------------------------- | ------------------------- |
| CaseCoreSupportWidget | SupportWidget |
| CaseCoreContactWidget | ContactWidget |
| useCaseCoreSupportForm | useSupportForm |
| caseCoreSupportAttachmentLimits | supportAttachmentLimits |
| caseCoreSupportWidgetVersion | supportWidgetVersion |
| .casecore-support__* (классы) | .support-form__* |
| --cc-* (переменные) | --support-* |
Совместимости со старыми именами нет намеренно: продукт правит импорты один раз, а не носит два словаря. HTTP-контракт с BFF не изменился.
Public contact form
ContactWidget is the anonymous marketing-site variant. It renders name,
email, company, topic, message, consent, and a hidden website bot-trap field.
Supply the current public Turnstile result through turnstileToken; the same-origin
BFF passes it to intake-client.createLead().
<ContactWidget
endpoint="/api/contact"
locale="en"
turnstileToken={turnstileToken}
pageUrl={window.location.href}
campaign="summer-demo"
/>The component exposes no CaseCore credential prop.
