@ebitex/forms-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ import type { SdkForm } from './types.js';
2
+ export interface FormsClientOptions {
3
+ /**
4
+ * An organization-scoped API key, created in Hub under Organizations → API Keys.
5
+ *
6
+ * **Server-side only.** This key acts for the whole organization and is not scoped to one form.
7
+ * Anything you ship to a browser is readable by every visitor, so fetch form definitions from
8
+ * your own server or build step and pass the resulting object to the rendering components — which
9
+ * never receive the key. That split is why `@ebitex/forms-sdk/react` is a separate entry point.
10
+ */
11
+ apiKey: string;
12
+ /** Defaults to ebitex's production API. Override for a self-hosted or local deployment. */
13
+ baseUrl?: string;
14
+ /** Injectable for testing or for a runtime with a non-global fetch. */
15
+ fetch?: typeof globalThis.fetch;
16
+ }
17
+ /**
18
+ * A non-2xx response from the Forms API. `status` and `body` are exposed so callers can branch on
19
+ * the specific failure rather than parsing a message: 401 (bad or revoked key), 402 (the
20
+ * organization's subscription needs attention), 404 (no such form for this organization), 409 (the
21
+ * form has two fields sharing a submission key and must be fixed in the Forms editor), 429 (rate
22
+ * limited — a `Retry-After` header accompanies it).
23
+ */
24
+ export declare class FormsApiError extends Error {
25
+ readonly status: number;
26
+ readonly body: unknown;
27
+ constructor(status: number, body: unknown);
28
+ }
29
+ export interface FormsClient {
30
+ getForm(externalId: string): Promise<SdkForm>;
31
+ }
32
+ export declare function createFormsClient(options: FormsClientOptions): FormsClient;
33
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEzC,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd,2FAA2F;IAC3F,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAChC;AAED;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;CAM1C;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC9C;AAID,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CA4B1E"}
package/dist/client.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A non-2xx response from the Forms API. `status` and `body` are exposed so callers can branch on
3
+ * the specific failure rather than parsing a message: 401 (bad or revoked key), 402 (the
4
+ * organization's subscription needs attention), 404 (no such form for this organization), 409 (the
5
+ * form has two fields sharing a submission key and must be fixed in the Forms editor), 429 (rate
6
+ * limited — a `Retry-After` header accompanies it).
7
+ */
8
+ export class FormsApiError extends Error {
9
+ status;
10
+ body;
11
+ constructor(status, body) {
12
+ super(`ebitex Forms API request failed with status ${status}`);
13
+ this.name = 'FormsApiError';
14
+ this.status = status;
15
+ this.body = body;
16
+ }
17
+ }
18
+ const DEFAULT_BASE_URL = 'https://api.ebitex.io';
19
+ export function createFormsClient(options) {
20
+ const { apiKey } = options;
21
+ if (!apiKey) {
22
+ throw new Error('createFormsClient requires an apiKey.');
23
+ }
24
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
25
+ const doFetch = options.fetch ?? globalThis.fetch;
26
+ return {
27
+ async getForm(externalId) {
28
+ const response = await doFetch(`${baseUrl}/forms/v1/sdk/forms/${encodeURIComponent(externalId)}`, {
29
+ headers: {
30
+ Authorization: `Bearer ${apiKey}`,
31
+ Accept: 'application/json',
32
+ },
33
+ });
34
+ if (!response.ok) {
35
+ throw new FormsApiError(response.status, await readBody(response));
36
+ }
37
+ return (await response.json());
38
+ },
39
+ };
40
+ }
41
+ /** Error bodies are JSON in every documented case, but a proxy or gateway can return HTML — don't throw while building an error. */
42
+ async function readBody(response) {
43
+ try {
44
+ return await response.json();
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AACH,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,CAAQ;IACd,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAa;QACvC,KAAK,CAAC,+CAA+C,MAAM,EAAE,CAAC,CAAA;QAC9D,IAAI,CAAC,IAAI,GAAG,eAAe,CAAA;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAMD,MAAM,gBAAgB,GAAG,uBAAuB,CAAA;AAEhD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;IAC1D,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IACzE,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAA;IAEjD,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,UAAkB;YAC9B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAC5B,GAAG,OAAO,uBAAuB,kBAAkB,CAAC,UAAU,CAAC,EAAE,EACjE;gBACE,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,MAAM,EAAE;oBACjC,MAAM,EAAE,kBAAkB;iBAC3B;aACF,CACF,CAAA;YAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;YACpE,CAAC;YAED,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAY,CAAA;QAC3C,CAAC;KACF,CAAA;AACH,CAAC;AAED,oIAAoI;AACpI,KAAK,UAAU,QAAQ,CAAC,QAAkB;IACxC,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@ebitex/forms-sdk` — the framework-agnostic half: fetching a form definition and validating a
3
+ * submission against it. Neither imports React, so a server-only consumer never pulls it in.
4
+ *
5
+ * The rendering components live behind a separate entry point, `@ebitex/forms-sdk/react`. That
6
+ * split is deliberate and load-bearing: `createFormsClient` takes an organization-wide API key and
7
+ * must only ever run server-side, while the components render in a respondent's browser and must
8
+ * never receive one. Expressing the boundary in the import graph makes a mistake visible in review.
9
+ */
10
+ export { createFormsClient, FormsApiError, type FormsClient, type FormsClientOptions, } from './client.js';
11
+ export { validateSubmission, type ValidateOptions } from './validators.js';
12
+ export { collectDataFields, isDataField, isFormField, type ButtonField, type ButtonType, type ChoiceField, type ChoiceOption, type ChoiceType, type FieldsetNode, type FormField, type FormNode, type FormStep, type InputField, type InputVariant, type NumberField, type RowNode, type SdkForm, type SubmissionData, type TextareaField, type ValidationErrors, } from './types.js';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE1E,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@ebitex/forms-sdk` — the framework-agnostic half: fetching a form definition and validating a
3
+ * submission against it. Neither imports React, so a server-only consumer never pulls it in.
4
+ *
5
+ * The rendering components live behind a separate entry point, `@ebitex/forms-sdk/react`. That
6
+ * split is deliberate and load-bearing: `createFormsClient` takes an organization-wide API key and
7
+ * must only ever run server-side, while the components render in a respondent's browser and must
8
+ * never receive one. Expressing the boundary in the import graph makes a mistake visible in review.
9
+ */
10
+ export { createFormsClient, FormsApiError, } from './client.js';
11
+ export { validateSubmission } from './validators.js';
12
+ export { collectDataFields, isDataField, isFormField, } from './types.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,iBAAiB,EACjB,aAAa,GAGd,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,kBAAkB,EAAwB,MAAM,iBAAiB,CAAA;AAE1E,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,WAAW,GAkBZ,MAAM,YAAY,CAAA"}
@@ -0,0 +1,36 @@
1
+ import { type CSSProperties } from 'react';
2
+ import { type SdkForm, type SubmissionData } from '../types.js';
3
+ import { type FieldComponents } from './fields.js';
4
+ export interface EbitexFormProps {
5
+ /** A form definition from `createFormsClient(...).getForm(...)`, fetched on your server. */
6
+ form: SdkForm;
7
+ /** Called after a successful final submission. */
8
+ onSubmitted?: (data: SubmissionData) => void;
9
+ /** Called when a submission request fails. Validation failures are shown inline and don't call this. */
10
+ onError?: (error: unknown) => void;
11
+ className?: string;
12
+ style?: CSSProperties;
13
+ /**
14
+ * Replace the component used for one or more field types. Anything not overridden keeps its
15
+ * default, so this is a per-field escape hatch rather than an all-or-nothing takeover — override
16
+ * `Input` alone and your Choice fields still render normally.
17
+ */
18
+ fieldComponents?: FieldComponents;
19
+ /** Injectable for tests and non-standard runtimes. */
20
+ fetch?: typeof globalThis.fetch;
21
+ }
22
+ /**
23
+ * Renders an ebitex Form and submits it.
24
+ *
25
+ * **This component never sees your API key**, by design. It posts to the form's own
26
+ * `submissionUrl` — the public, anonymous submission endpoint — exactly as ebitex's own hosted page
27
+ * and iframe embed do. An organization-wide credential shipped to a respondent's browser would be
28
+ * readable by every visitor; fetch the definition server-side with `createFormsClient` and pass the
29
+ * result here.
30
+ *
31
+ * Multi-step forms save progress as the respondent advances, so a partially-completed submission
32
+ * isn't lost — matching the behaviour of ebitex's own renderer rather than a simplified
33
+ * client-only notion of steps.
34
+ */
35
+ export declare function EbitexForm({ form, onSubmitted, onError, className, style, fieldComponents, fetch: injectedFetch, }: EbitexFormProps): import("react").JSX.Element | null;
36
+ //# sourceMappingURL=EbitexForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EbitexForm.d.ts","sourceRoot":"","sources":["../../src/react/EbitexForm.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AACpE,OAAO,EAGL,KAAK,OAAO,EACZ,KAAK,cAAc,EAEpB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AAGpB,MAAM,WAAW,eAAe;IAC9B,4FAA4F;IAC5F,IAAI,EAAE,OAAO,CAAA;IAEb,kDAAkD;IAClD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;IAE5C,wGAAwG;IACxG,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAElC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IAErB;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAA;IAEjC,sDAAsD;IACtD,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAChC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,WAAW,EACX,OAAO,EACP,SAAS,EACT,KAAK,EACL,eAAe,EACf,KAAK,EAAE,aAAa,GACrB,EAAE,eAAe,sCAoLjB"}
@@ -0,0 +1,174 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useId, useMemo, useState } from 'react';
3
+ import { collectDataFields, } from '../types.js';
4
+ import { validateSubmission } from '../validators.js';
5
+ import { ChoiceFieldControl, InputFieldControl, NumberFieldControl, TextareaFieldControl, } from './fields.js';
6
+ import { getOrCreateVisitorId } from './visitorId.js';
7
+ /**
8
+ * Renders an ebitex Form and submits it.
9
+ *
10
+ * **This component never sees your API key**, by design. It posts to the form's own
11
+ * `submissionUrl` — the public, anonymous submission endpoint — exactly as ebitex's own hosted page
12
+ * and iframe embed do. An organization-wide credential shipped to a respondent's browser would be
13
+ * readable by every visitor; fetch the definition server-side with `createFormsClient` and pass the
14
+ * result here.
15
+ *
16
+ * Multi-step forms save progress as the respondent advances, so a partially-completed submission
17
+ * isn't lost — matching the behaviour of ebitex's own renderer rather than a simplified
18
+ * client-only notion of steps.
19
+ */
20
+ export function EbitexForm({ form, onSubmitted, onError, className, style, fieldComponents, fetch: injectedFetch, }) {
21
+ const baseId = useId();
22
+ const doFetch = injectedFetch ?? globalThis.fetch;
23
+ const [stepIndex, setStepIndex] = useState(0);
24
+ const [data, setData] = useState({});
25
+ const [errors, setErrors] = useState({});
26
+ const [submitting, setSubmitting] = useState(false);
27
+ const [completed, setCompleted] = useState(false);
28
+ const steps = form.steps;
29
+ const step = steps[stepIndex];
30
+ const isLastStep = stepIndex === steps.length - 1;
31
+ const visitorId = useMemo(() => getOrCreateVisitorId(), []);
32
+ if (completed) {
33
+ return (_jsx("div", { className: joinClassNames('ebitex-forms', className), style: style, children: _jsx("p", { className: "ebitex-forms__complete", role: "status", children: "Thanks \u2014 your response has been submitted." }) }));
34
+ }
35
+ if (!step) {
36
+ return null;
37
+ }
38
+ /**
39
+ * Validates only the current step's fields. Validating the whole form on a step advance would
40
+ * report errors for fields the respondent hasn't reached yet.
41
+ */
42
+ function validateCurrentStep(requireAll) {
43
+ const stepKeys = new Set(collectDataFields(step.nodes)
44
+ .map((field) => field.submissionKey)
45
+ .filter((key) => key !== undefined));
46
+ const all = validateSubmission(form, data, { requireAll });
47
+ return Object.fromEntries(Object.entries(all).filter(([key]) => stepKeys.has(key)));
48
+ }
49
+ async function save(complete) {
50
+ setSubmitting(true);
51
+ try {
52
+ const response = await doFetch(form.submissionUrl, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify({ visitor: visitorId, data, complete }),
56
+ });
57
+ if (!response.ok) {
58
+ // The server is authoritative: a field-level rejection is surfaced inline rather than
59
+ // thrown, so a respondent can fix it. Anything else is the host app's to handle.
60
+ const body = (await response.json().catch(() => null));
61
+ if (body?.error === 'validation_failed' && body.errors) {
62
+ setErrors(body.errors);
63
+ return false;
64
+ }
65
+ onError?.(new Error(`Submission failed with status ${response.status}`));
66
+ return false;
67
+ }
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ onError?.(error);
72
+ return false;
73
+ }
74
+ finally {
75
+ setSubmitting(false);
76
+ }
77
+ }
78
+ async function advance() {
79
+ const stepErrors = validateCurrentStep(true);
80
+ if (Object.keys(stepErrors).length > 0) {
81
+ setErrors(stepErrors);
82
+ return;
83
+ }
84
+ setErrors({});
85
+ // A partial save, so progress survives a respondent leaving mid-form.
86
+ if (await save(false)) {
87
+ setStepIndex((current) => current + 1);
88
+ }
89
+ }
90
+ async function submit() {
91
+ const allErrors = validateSubmission(form, data, { requireAll: true });
92
+ if (Object.keys(allErrors).length > 0) {
93
+ setErrors(allErrors);
94
+ return;
95
+ }
96
+ setErrors({});
97
+ if (await save(true)) {
98
+ setCompleted(true);
99
+ onSubmitted?.(data);
100
+ }
101
+ }
102
+ function setValue(key, value) {
103
+ setData((current) => ({ ...current, [key]: value }));
104
+ }
105
+ const disabled = submitting || !form.submittable;
106
+ return (_jsxs("div", { className: joinClassNames('ebitex-forms', className), style: style, children: [!form.submittable ? (_jsxs("p", { className: "ebitex-forms__notice", role: "status", children: ["This form is ", form.status.toLowerCase(), " and isn't accepting responses yet. It's shown here for preview only."] })) : null, _jsxs("form", { noValidate: true, onSubmit: (event) => {
107
+ event.preventDefault();
108
+ if (isLastStep) {
109
+ void submit();
110
+ }
111
+ else {
112
+ void advance();
113
+ }
114
+ }, children: [steps.length > 1 ? (_jsxs("p", { className: "ebitex-forms__step-indicator", role: "status", children: ["Step ", stepIndex + 1, " of ", steps.length, step.label ? `: ${step.label}` : ''] })) : null, _jsx(NodeList, { nodes: step.nodes, baseId: baseId, data: data, errors: errors, disabled: disabled, fieldComponents: fieldComponents, onChange: setValue }), _jsxs("div", { className: "ebitex-forms__actions", children: [stepIndex > 0 ? (_jsx("button", { type: "button", className: "ebitex-forms__button ebitex-forms__button--secondary", disabled: submitting,
115
+ // Deliberately does not re-save: going back is navigation, not a change.
116
+ onClick: () => setStepIndex((current) => current - 1), children: "Back" })) : null, isLastStep ? (_jsx(ActionButtons, { nodes: form.actions, disabled: disabled })) : (_jsx("button", { type: "submit", className: "ebitex-forms__button", disabled: disabled, children: "Next" }))] })] })] }));
117
+ }
118
+ /** The Form's own configured Submit/Reset controls, shown on the final step. */
119
+ function ActionButtons({ nodes, disabled }) {
120
+ const buttons = nodes.filter((node) => node.nodeType === 'Field' && node.fieldType === 'Button');
121
+ if (buttons.length === 0) {
122
+ return (_jsx("button", { type: "submit", className: "ebitex-forms__button", disabled: disabled, children: "Submit" }));
123
+ }
124
+ return (_jsx(_Fragment, { children: buttons.map((button, index) => (_jsx("button", { type: button.buttonType === 'Reset' ? 'reset' : button.buttonType === 'Button' ? 'button' : 'submit', className: joinClassNames('ebitex-forms__button', button.buttonType !== 'Submit' ? 'ebitex-forms__button--secondary' : undefined), disabled: disabled, children: button.label }, `${button.label}-${index}`))) }));
125
+ }
126
+ function NodeList({ nodes, baseId, data, errors, disabled, fieldComponents, onChange, }) {
127
+ return (_jsx(_Fragment, { children: nodes.map((node, index) => {
128
+ if (node.nodeType === 'Fieldset') {
129
+ return (_jsxs("fieldset", { className: "ebitex-forms__fieldset", children: [_jsx("legend", { children: node.legend }), _jsx(NodeList, { nodes: node.nodes, baseId: baseId, data: data, errors: errors, disabled: disabled, fieldComponents: fieldComponents, onChange: onChange })] }, `fieldset-${index}`));
130
+ }
131
+ if (node.nodeType === 'Row') {
132
+ // Horizontal on wide viewports, single column on narrow — see styles.css.
133
+ return (_jsx("div", { className: "ebitex-forms__row", children: _jsx(NodeList, { nodes: node.nodes, baseId: baseId, data: data, errors: errors, disabled: disabled, fieldComponents: fieldComponents, onChange: onChange }) }, `row-${index}`));
134
+ }
135
+ if (node.fieldType === 'Button') {
136
+ // Buttons inside a step's tree are the Form's own Actions, rendered by ActionButtons.
137
+ return null;
138
+ }
139
+ const key = node.submissionKey;
140
+ if (key === undefined)
141
+ return null;
142
+ const id = `${baseId}-${key}`;
143
+ const error = errors[key] ?? null;
144
+ const shared = {
145
+ id,
146
+ value: data[key] ?? null,
147
+ error,
148
+ disabled,
149
+ onChange: (value) => onChange(key, value),
150
+ };
151
+ switch (node.fieldType) {
152
+ case 'Input': {
153
+ const Component = fieldComponents?.Input ?? InputFieldControl;
154
+ return _jsx(Component, { field: node, ...shared }, key);
155
+ }
156
+ case 'Textarea': {
157
+ const Component = fieldComponents?.Textarea ?? TextareaFieldControl;
158
+ return _jsx(Component, { field: node, ...shared }, key);
159
+ }
160
+ case 'Number': {
161
+ const Component = fieldComponents?.Number ?? NumberFieldControl;
162
+ return _jsx(Component, { field: node, ...shared }, key);
163
+ }
164
+ case 'Choice': {
165
+ const Component = fieldComponents?.Choice ?? ChoiceFieldControl;
166
+ return _jsx(Component, { field: node, ...shared }, key);
167
+ }
168
+ }
169
+ }) }));
170
+ }
171
+ function joinClassNames(...values) {
172
+ return values.filter(Boolean).join(' ');
173
+ }
174
+ //# sourceMappingURL=EbitexForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EbitexForm.js","sourceRoot":"","sources":["../../src/react/EbitexForm.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { ChoiceField, InputField, NumberField, SubmissionData, TextareaField } from '../types.js';
2
+ /**
3
+ * What every field component receives. A consumer overriding one via `fieldComponents` implements
4
+ * this same contract, so an override is a drop-in rather than a fork of the surrounding form.
5
+ */
6
+ export interface FieldComponentProps<TField> {
7
+ field: TField;
8
+ /** Stable, unique per field — wire this to both the control's `id` and its label's `htmlFor`. */
9
+ id: string;
10
+ value: SubmissionData[string];
11
+ onChange: (value: SubmissionData[string]) => void;
12
+ /** A validation message for this field, or `null`. Render it and reference it via `aria-describedby`. */
13
+ error: string | null;
14
+ disabled: boolean;
15
+ }
16
+ export interface FieldComponents {
17
+ Input?: React.ComponentType<FieldComponentProps<InputField>>;
18
+ Textarea?: React.ComponentType<FieldComponentProps<TextareaField>>;
19
+ Number?: React.ComponentType<FieldComponentProps<NumberField>>;
20
+ Choice?: React.ComponentType<FieldComponentProps<ChoiceField>>;
21
+ }
22
+ export declare function InputFieldControl({ field, id, value, onChange, error, disabled }: FieldComponentProps<InputField>): import("react").JSX.Element;
23
+ export declare function TextareaFieldControl({ field, id, value, onChange, error, disabled }: FieldComponentProps<TextareaField>): import("react").JSX.Element;
24
+ export declare function NumberFieldControl({ field, id, value, onChange, error, disabled }: FieldComponentProps<NumberField>): import("react").JSX.Element;
25
+ export declare function ChoiceFieldControl({ field, id, value, onChange, error, disabled }: FieldComponentProps<ChoiceField>): import("react").JSX.Element;
26
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../src/react/fields.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,WAAW,EACX,cAAc,EACd,aAAa,EACd,MAAM,aAAa,CAAA;AAEpB;;;GAGG;AACH,MAAM,WAAW,mBAAmB,CAAC,MAAM;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,iGAAiG;IACjG,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,CAAA;IAC7B,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,IAAI,CAAA;IACjD,yGAAyG;IACzG,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC,CAAA;IAC5D,QAAQ,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,mBAAmB,CAAC,aAAa,CAAC,CAAC,CAAA;IAClE,MAAM,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC,CAAA;IAC9D,MAAM,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC,CAAA;CAC/D;AAuCD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,mBAAmB,CAAC,UAAU,CAAC,+BAsBjH;AAED,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,mBAAmB,CAAC,aAAa,CAAC,+BAsBvH;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,mBAAmB,CAAC,WAAW,CAAC,+BA2BnH;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,mBAAmB,CAAC,WAAW,CAAC,+BAsFnH"}
@@ -0,0 +1,57 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ const INPUT_TYPES = {
3
+ Text: 'text',
4
+ Email: 'email',
5
+ Tel: 'tel',
6
+ Url: 'url',
7
+ Password: 'password',
8
+ };
9
+ /**
10
+ * A real `<label for>` rather than a placeholder or an aria-label: a placeholder disappears on
11
+ * focus and is not reliably announced, and this is the one surface an organization's own customers
12
+ * use directly.
13
+ */
14
+ function Label({ id, children, required }) {
15
+ return (_jsxs("label", { className: "ebitex-forms__label", htmlFor: id, children: [children, required ? (_jsxs("span", { className: "ebitex-forms__required", "aria-hidden": "true", children: [' ', "*"] })) : null] }));
16
+ }
17
+ function ErrorMessage({ id, error }) {
18
+ if (error === null)
19
+ return null;
20
+ return (_jsx("p", { className: "ebitex-forms__error", id: `${id}-error`, children: error }));
21
+ }
22
+ export function InputFieldControl({ field, id, value, onChange, error, disabled }) {
23
+ return (_jsxs("div", { className: "ebitex-forms__field", children: [_jsx(Label, { id: id, required: field.required, children: field.label }), _jsx("input", { className: "ebitex-forms__control", id: id, type: INPUT_TYPES[field.variant], value: typeof value === 'string' ? value : '', placeholder: field.placeholder ?? undefined, required: field.required, "aria-required": field.required, "aria-invalid": error !== null, "aria-describedby": error !== null ? `${id}-error` : undefined, disabled: disabled, onChange: (event) => onChange(event.target.value) }), _jsx(ErrorMessage, { id: id, error: error })] }));
24
+ }
25
+ export function TextareaFieldControl({ field, id, value, onChange, error, disabled }) {
26
+ return (_jsxs("div", { className: "ebitex-forms__field", children: [_jsx(Label, { id: id, required: field.required, children: field.label }), _jsx("textarea", { className: "ebitex-forms__control", id: id, rows: field.rows ?? 4, value: typeof value === 'string' ? value : '', placeholder: field.placeholder ?? undefined, required: field.required, "aria-required": field.required, "aria-invalid": error !== null, "aria-describedby": error !== null ? `${id}-error` : undefined, disabled: disabled, onChange: (event) => onChange(event.target.value) }), _jsx(ErrorMessage, { id: id, error: error })] }));
27
+ }
28
+ export function NumberFieldControl({ field, id, value, onChange, error, disabled }) {
29
+ return (_jsxs("div", { className: "ebitex-forms__field", children: [_jsx(Label, { id: id, required: field.required, children: field.label }), _jsx("input", { className: "ebitex-forms__control", id: id, type: "number", value: value === null || value === undefined ? '' : String(value), placeholder: field.placeholder ?? undefined, min: field.min ?? undefined, max: field.max ?? undefined, step: field.step ?? undefined, required: field.required, "aria-required": field.required, "aria-invalid": error !== null, "aria-describedby": error !== null ? `${id}-error` : undefined, disabled: disabled,
30
+ // Kept as a string rather than coerced here: the server accepts a numeric string, and
31
+ // coercing would turn a half-typed "-" or "1." into NaN mid-keystroke.
32
+ onChange: (event) => onChange(event.target.value) }), _jsx(ErrorMessage, { id: id, error: error })] }));
33
+ }
34
+ export function ChoiceFieldControl({ field, id, value, onChange, error, disabled }) {
35
+ const describedBy = error !== null ? `${id}-error` : undefined;
36
+ if (field.choiceType === 'Select') {
37
+ return (_jsxs("div", { className: "ebitex-forms__field", children: [_jsx(Label, { id: id, required: field.required, children: field.label }), _jsxs("select", { className: "ebitex-forms__control", id: id, value: typeof value === 'string' ? value : '', required: field.required, "aria-required": field.required, "aria-invalid": error !== null, "aria-describedby": describedBy, disabled: disabled, onChange: (event) => onChange(event.target.value), children: [_jsx("option", { value: "", children: "Select\u2026" }), field.options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] }), _jsx(ErrorMessage, { id: id, error: error })] }));
38
+ }
39
+ const selected = Array.isArray(value) ? value : typeof value === 'string' && value !== '' ? [value] : [];
40
+ // A radio/checkbox group is a group of controls, not one labelled control — so the visible label
41
+ // becomes a <legend> inside a <fieldset>, which is what actually associates it with every option
42
+ // for a screen reader.
43
+ return (_jsxs("fieldset", { className: "ebitex-forms__field ebitex-forms__group", "aria-required": field.required, "aria-invalid": error !== null, "aria-describedby": describedBy, disabled: disabled, children: [_jsxs("legend", { className: "ebitex-forms__label", children: [field.label, field.required ? (_jsxs("span", { className: "ebitex-forms__required", "aria-hidden": "true", children: [' ', "*"] })) : null] }), field.options.map((option) => {
44
+ const optionId = `${id}-${option.value}`;
45
+ const isCheckbox = field.choiceType === 'Checkbox';
46
+ return (_jsxs("div", { className: "ebitex-forms__option", children: [_jsx("input", { id: optionId, type: isCheckbox ? 'checkbox' : 'radio', name: id, value: option.value, checked: selected.includes(option.value), onChange: (event) => {
47
+ if (!isCheckbox) {
48
+ onChange(option.value);
49
+ return;
50
+ }
51
+ onChange(event.target.checked
52
+ ? [...selected, option.value]
53
+ : selected.filter((item) => item !== option.value));
54
+ } }), _jsx("label", { htmlFor: optionId, children: option.label })] }, option.value));
55
+ }), _jsx(ErrorMessage, { id: id, error: error })] }));
56
+ }
57
+ //# sourceMappingURL=fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.js","sourceRoot":"","sources":["../../src/react/fields.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `@ebitex/forms-sdk/react` — the rendering half.
3
+ *
4
+ * Nothing here takes an API key, deliberately: these components run in a respondent's browser and
5
+ * submit to the form's own public, anonymous endpoint. Fetch the definition server-side with
6
+ * `createFormsClient` from the root entry point and pass the result to `<EbitexForm />`.
7
+ */
8
+ export { EbitexForm, type EbitexFormProps } from './EbitexForm.js';
9
+ export { ChoiceFieldControl, InputFieldControl, NumberFieldControl, TextareaFieldControl, type FieldComponentProps, type FieldComponents, } from './fields.js';
10
+ export { getOrCreateVisitorId } from './visitorId.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAElE,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `@ebitex/forms-sdk/react` — the rendering half.
3
+ *
4
+ * Nothing here takes an API key, deliberately: these components run in a respondent's browser and
5
+ * submit to the form's own public, anonymous endpoint. Fetch the definition server-side with
6
+ * `createFormsClient` from the root entry point and pass the result to `<EbitexForm />`.
7
+ */
8
+ export { EbitexForm } from './EbitexForm.js';
9
+ export { ChoiceFieldControl, InputFieldControl, NumberFieldControl, TextareaFieldControl, } from './fields.js';
10
+ export { getOrCreateVisitorId } from './visitorId.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAwB,MAAM,iBAAiB,CAAA;AAElE,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,GAGrB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Optional default styling for @ebitex/forms-sdk's React components.
3
+ *
4
+ * Import it (`import '@ebitex/forms-sdk/styles.css'`) for a usable form out of the box, or skip it
5
+ * entirely and target the `.ebitex-forms__*` class names with your own CSS. Every value below is a
6
+ * custom property, so the common case — matching your own design system — needs neither a fork nor
7
+ * a build step:
8
+ *
9
+ * .ebitex-forms {
10
+ * --ebitex-forms-accent: #7c3aed;
11
+ * --ebitex-forms-radius: 0;
12
+ * }
13
+ *
14
+ * Deliberately unopinionated: no font stack (inherits yours), no imposed max-width, no colour
15
+ * scheme beyond what the tokens set. A form embedded in someone else's page should look like it
16
+ * belongs there, and the most common complaint about an embeddable widget is that it doesn't.
17
+ */
18
+
19
+ .ebitex-forms {
20
+ --ebitex-forms-accent: #2563eb;
21
+ --ebitex-forms-accent-contrast: #ffffff;
22
+ --ebitex-forms-text: #111827;
23
+ --ebitex-forms-muted: #6b7280;
24
+ --ebitex-forms-border: #d1d5db;
25
+ --ebitex-forms-error: #dc2626;
26
+ --ebitex-forms-surface: transparent;
27
+ --ebitex-forms-radius: 0.375rem;
28
+ --ebitex-forms-gap: 1rem;
29
+
30
+ color: var(--ebitex-forms-text);
31
+ background: var(--ebitex-forms-surface);
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: var(--ebitex-forms-gap);
35
+ }
36
+
37
+ /* Follows the host page when it hasn't set an explicit theme, rather than forcing light. */
38
+ @media (prefers-color-scheme: dark) {
39
+ .ebitex-forms:not([data-ebitex-theme='light']) {
40
+ --ebitex-forms-text: #f3f4f6;
41
+ --ebitex-forms-muted: #9ca3af;
42
+ --ebitex-forms-border: #4b5563;
43
+ --ebitex-forms-error: #f87171;
44
+ }
45
+ }
46
+
47
+ .ebitex-forms form {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: var(--ebitex-forms-gap);
51
+ }
52
+
53
+ .ebitex-forms__field {
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 0.375rem;
57
+ min-width: 0;
58
+ }
59
+
60
+ .ebitex-forms__label {
61
+ font-size: 0.875rem;
62
+ font-weight: 500;
63
+ }
64
+
65
+ .ebitex-forms__required {
66
+ color: var(--ebitex-forms-error);
67
+ }
68
+
69
+ .ebitex-forms__control {
70
+ font: inherit;
71
+ color: inherit;
72
+ background: transparent;
73
+ border: 1px solid var(--ebitex-forms-border);
74
+ border-radius: var(--ebitex-forms-radius);
75
+ padding: 0.5rem 0.625rem;
76
+ width: 100%;
77
+ box-sizing: border-box;
78
+ }
79
+
80
+ .ebitex-forms__control:focus-visible {
81
+ outline: 2px solid var(--ebitex-forms-accent);
82
+ outline-offset: 1px;
83
+ }
84
+
85
+ .ebitex-forms__control[aria-invalid='true'] {
86
+ border-color: var(--ebitex-forms-error);
87
+ }
88
+
89
+ /* Without this a disabled control is indistinguishable from an editable one in most browsers'
90
+ default styling — which matters here because the whole point of the non-submittable preview
91
+ state is that it should be visibly not-fillable. Caught by looking at a rendered Draft form,
92
+ not by a test: every disabled assertion passed while the form still looked interactive. */
93
+ .ebitex-forms__control:disabled,
94
+ .ebitex-forms__group:disabled {
95
+ opacity: 0.6;
96
+ cursor: not-allowed;
97
+ background: color-mix(in srgb, var(--ebitex-forms-border) 20%, transparent);
98
+ }
99
+
100
+ .ebitex-forms__error {
101
+ color: var(--ebitex-forms-error);
102
+ font-size: 0.8125rem;
103
+ margin: 0;
104
+ }
105
+
106
+ .ebitex-forms__notice,
107
+ .ebitex-forms__step-indicator {
108
+ color: var(--ebitex-forms-muted);
109
+ font-size: 0.875rem;
110
+ margin: 0;
111
+ }
112
+
113
+ .ebitex-forms__complete {
114
+ font-size: 0.9375rem;
115
+ margin: 0;
116
+ }
117
+
118
+ .ebitex-forms__fieldset,
119
+ .ebitex-forms__group {
120
+ border: 1px solid var(--ebitex-forms-border);
121
+ border-radius: var(--ebitex-forms-radius);
122
+ padding: 0.875rem;
123
+ margin: 0;
124
+ min-width: 0;
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: 0.5rem;
128
+ }
129
+
130
+ .ebitex-forms__group {
131
+ border: 0;
132
+ padding: 0;
133
+ }
134
+
135
+ .ebitex-forms__option {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 0.5rem;
139
+ font-size: 0.9375rem;
140
+ }
141
+
142
+ /* Horizontal, collapsing to one column on narrow viewports — matching how Row renders in the
143
+ ebitex builder and on the hosted page. */
144
+ .ebitex-forms__row {
145
+ display: grid;
146
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
147
+ gap: var(--ebitex-forms-gap);
148
+ }
149
+
150
+ .ebitex-forms__actions {
151
+ display: flex;
152
+ flex-wrap: wrap;
153
+ gap: 0.5rem;
154
+ }
155
+
156
+ .ebitex-forms__button {
157
+ font: inherit;
158
+ cursor: pointer;
159
+ border: 1px solid var(--ebitex-forms-accent);
160
+ border-radius: var(--ebitex-forms-radius);
161
+ background: var(--ebitex-forms-accent);
162
+ color: var(--ebitex-forms-accent-contrast);
163
+ padding: 0.5rem 1rem;
164
+ }
165
+
166
+ .ebitex-forms__button--secondary {
167
+ background: transparent;
168
+ color: inherit;
169
+ border-color: var(--ebitex-forms-border);
170
+ }
171
+
172
+ .ebitex-forms__button:disabled {
173
+ opacity: 0.6;
174
+ cursor: not-allowed;
175
+ }
176
+
177
+ .ebitex-forms__button:focus-visible {
178
+ outline: 2px solid var(--ebitex-forms-accent);
179
+ outline-offset: 2px;
180
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The persistent visitor id the public submission endpoint uses to reconcile a respondent's
3
+ * progress across steps.
4
+ *
5
+ * Deliberately a small reimplementation of `forms-web`'s own `lib/visitorId.ts` rather than a
6
+ * shared import: this package must have zero dependency on ebitex's internal workspace packages —
7
+ * its consumers are arbitrary external apps with their own bundlers and design systems — so a
8
+ * shared module would have to become a second published package to serve one caller.
9
+ *
10
+ * Note the id is scoped to *your* site's origin, not ebitex's, because nothing here loads in an
11
+ * iframe. A respondent who fills the same form on two different sites is therefore two visitors.
12
+ *
13
+ * Never throws. Storage access can fail outright under some privacy settings, and a form that
14
+ * refuses to render because it couldn't save an id would be a far worse outcome than one that
15
+ * simply can't reconcile progress.
16
+ */
17
+ export declare function getOrCreateVisitorId(): string;
18
+ /** Test seam — resets the in-memory fallback between cases. */
19
+ export declare function resetVisitorIdFallbackForTests(): void;
20
+ //# sourceMappingURL=visitorId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitorId.d.ts","sourceRoot":"","sources":["../../src/react/visitorId.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAY7C;AAED,+DAA+D;AAC/D,wBAAgB,8BAA8B,IAAI,IAAI,CAErD"}
@@ -0,0 +1,42 @@
1
+ const STORAGE_KEY = 'ebitex.visitor-id';
2
+ /**
3
+ * Held for the life of the page when storage is unavailable, so a respondent in a privacy-mode
4
+ * browser still gets consistent behaviour *within* one visit (a multi-step form keeps accumulating
5
+ * into one submission) even though nothing persists across visits.
6
+ */
7
+ let inMemoryFallback = null;
8
+ /**
9
+ * The persistent visitor id the public submission endpoint uses to reconcile a respondent's
10
+ * progress across steps.
11
+ *
12
+ * Deliberately a small reimplementation of `forms-web`'s own `lib/visitorId.ts` rather than a
13
+ * shared import: this package must have zero dependency on ebitex's internal workspace packages —
14
+ * its consumers are arbitrary external apps with their own bundlers and design systems — so a
15
+ * shared module would have to become a second published package to serve one caller.
16
+ *
17
+ * Note the id is scoped to *your* site's origin, not ebitex's, because nothing here loads in an
18
+ * iframe. A respondent who fills the same form on two different sites is therefore two visitors.
19
+ *
20
+ * Never throws. Storage access can fail outright under some privacy settings, and a form that
21
+ * refuses to render because it couldn't save an id would be a far worse outcome than one that
22
+ * simply can't reconcile progress.
23
+ */
24
+ export function getOrCreateVisitorId() {
25
+ try {
26
+ const existing = window.localStorage.getItem(STORAGE_KEY);
27
+ if (existing)
28
+ return existing;
29
+ const created = crypto.randomUUID();
30
+ window.localStorage.setItem(STORAGE_KEY, created);
31
+ return created;
32
+ }
33
+ catch {
34
+ inMemoryFallback ??= crypto.randomUUID();
35
+ return inMemoryFallback;
36
+ }
37
+ }
38
+ /** Test seam — resets the in-memory fallback between cases. */
39
+ export function resetVisitorIdFallbackForTests() {
40
+ inMemoryFallback = null;
41
+ }
42
+ //# sourceMappingURL=visitorId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitorId.js","sourceRoot":"","sources":["../../src/react/visitorId.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,GAAG,mBAAmB,CAAA;AAEvC;;;;GAIG;AACH,IAAI,gBAAgB,GAAkB,IAAI,CAAA;AAE1C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,oBAAoB;IAClC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QACzD,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAA;QAE7B,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAA;QACnC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,CAAA;QACjD,OAAO,OAAO,CAAA;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,gBAAgB,KAAK,MAAM,CAAC,UAAU,EAAE,CAAA;QACxC,OAAO,gBAAgB,CAAA;IACzB,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,8BAA8B;IAC5C,gBAAgB,GAAG,IAAI,CAAA;AACzB,CAAC"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The wire shape of `GET /forms/v1/sdk/forms/{externalId}`.
3
+ *
4
+ * Transcribed from the endpoint's actual response, not from a spec document — the server serializes
5
+ * its resolved node tree through the same converter its persistence layer uses, so these types
6
+ * describe what genuinely arrives rather than what was intended to.
7
+ */
8
+ export type InputVariant = 'Text' | 'Email' | 'Tel' | 'Url' | 'Password';
9
+ export type ChoiceType = 'Select' | 'Radio' | 'Checkbox';
10
+ export type ButtonType = 'Submit' | 'Reset' | 'Button';
11
+ export interface ChoiceOption {
12
+ value: string;
13
+ label: string;
14
+ }
15
+ interface FieldBase {
16
+ nodeType: 'Field';
17
+ label: string;
18
+ /**
19
+ * The key this field's answer is submitted under. Absent on buttons, which carry no value.
20
+ *
21
+ * Deliberately **not** the same as `name`: a field placed by reference to a shared Field is keyed
22
+ * by that Field's external id instead. Always submit under this value — deriving a key any other
23
+ * way silently breaks plugin field mapping on the server.
24
+ */
25
+ submissionKey?: string;
26
+ }
27
+ export interface InputField extends FieldBase {
28
+ fieldType: 'Input';
29
+ name: string;
30
+ required: boolean;
31
+ variant: InputVariant;
32
+ placeholder?: string | null;
33
+ }
34
+ export interface TextareaField extends FieldBase {
35
+ fieldType: 'Textarea';
36
+ name: string;
37
+ required: boolean;
38
+ placeholder?: string | null;
39
+ rows?: number | null;
40
+ }
41
+ export interface NumberField extends FieldBase {
42
+ fieldType: 'Number';
43
+ name: string;
44
+ required: boolean;
45
+ placeholder?: string | null;
46
+ min?: number | null;
47
+ max?: number | null;
48
+ step?: number | null;
49
+ }
50
+ export interface ChoiceField extends FieldBase {
51
+ fieldType: 'Choice';
52
+ name: string;
53
+ required: boolean;
54
+ choiceType: ChoiceType;
55
+ /** Always a plain array — the server resolves data-set- and plugin-backed options before sending. */
56
+ options: ChoiceOption[];
57
+ }
58
+ export interface ButtonField extends FieldBase {
59
+ fieldType: 'Button';
60
+ buttonType: ButtonType;
61
+ }
62
+ export type FormField = InputField | TextareaField | NumberField | ChoiceField | ButtonField;
63
+ export interface FieldsetNode {
64
+ nodeType: 'Fieldset';
65
+ legend: string;
66
+ nodes: FormNode[];
67
+ }
68
+ export interface RowNode {
69
+ nodeType: 'Row';
70
+ nodes: FormNode[];
71
+ }
72
+ export type FormNode = FieldsetNode | RowNode | FormField;
73
+ export interface FormStep {
74
+ label: string;
75
+ nodes: FormNode[];
76
+ }
77
+ export interface SdkForm {
78
+ name: string;
79
+ externalId: string;
80
+ orgSlug: string;
81
+ /** Absolute URL of the public submission endpoint. Never send the API key here. */
82
+ submissionUrl: string;
83
+ status: 'Draft' | 'Active' | 'Archived';
84
+ /**
85
+ * Whether submissions will be accepted. `false` for a Draft or Archived form: the definition is
86
+ * retrievable so you can build against it before publishing, but the public submission endpoint
87
+ * gates on Active and would reject every submission.
88
+ */
89
+ submittable: boolean;
90
+ actions: FormNode[];
91
+ steps: FormStep[];
92
+ }
93
+ /** A respondent's answers, keyed by each field's `submissionKey`. */
94
+ export type SubmissionData = Record<string, string | string[] | number | null>;
95
+ /** Field-level validation messages, keyed by `submissionKey`. Empty means valid. */
96
+ export type ValidationErrors = Record<string, string>;
97
+ export declare function isFormField(node: FormNode): node is FormField;
98
+ /** Buttons are the one field shape carrying no answer, so they're excluded from data and validation. */
99
+ export declare function isDataField(node: FormNode): node is InputField | TextareaField | NumberField | ChoiceField;
100
+ /** Depth-first walk of a node tree, flattened to the data-carrying fields only. */
101
+ export declare function collectDataFields(nodes: FormNode[]): (InputField | TextareaField | NumberField | ChoiceField)[];
102
+ export {};
103
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The wire shape of `GET /forms/v1/sdk/forms/{externalId}`.
3
+ *
4
+ * Transcribed from the endpoint's actual response, not from a spec document — the server serializes
5
+ * its resolved node tree through the same converter its persistence layer uses, so these types
6
+ * describe what genuinely arrives rather than what was intended to.
7
+ */
8
+ export function isFormField(node) {
9
+ return node.nodeType === 'Field';
10
+ }
11
+ /** Buttons are the one field shape carrying no answer, so they're excluded from data and validation. */
12
+ export function isDataField(node) {
13
+ return isFormField(node) && node.fieldType !== 'Button';
14
+ }
15
+ /** Depth-first walk of a node tree, flattened to the data-carrying fields only. */
16
+ export function collectDataFields(nodes) {
17
+ const result = [];
18
+ for (const node of nodes) {
19
+ if (node.nodeType === 'Fieldset' || node.nodeType === 'Row') {
20
+ result.push(...collectDataFields(node.nodes));
21
+ }
22
+ else if (isDataField(node)) {
23
+ result.push(node);
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA6GH,MAAM,UAAU,WAAW,CAAC,IAAc;IACxC,OAAO,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAA;AAClC,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,WAAW,CACzB,IAAc;IAEd,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAA;AACzD,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAC/B,KAAiB;IAEjB,MAAM,MAAM,GAA+D,EAAE,CAAA;IAE7E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YAC5D,MAAM,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QAC/C,CAAC;aAAM,IAAI,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type SdkForm, type SubmissionData, type ValidationErrors } from './types.js';
2
+ export interface ValidateOptions {
3
+ /**
4
+ * `true` when finalizing a submission — every required field must be present and non-empty.
5
+ * `false` for an intermediate step save, which legitimately omits fields on later steps; only the
6
+ * shape of what *is* present gets checked.
7
+ */
8
+ requireAll: boolean;
9
+ }
10
+ /**
11
+ * A client-side port of the server's own `SubmissionDataValidator`, for immediate feedback before a
12
+ * step advances or a form submits.
13
+ *
14
+ * **The server remains authoritative.** These rules exist so a respondent sees a mistake without a
15
+ * round trip, not to be trusted — a client bypass still gets a real 400. They are kept deliberately
16
+ * identical to the server's, down to the message strings, and a shared fixture file
17
+ * (`src/test/fixtures/validation-cases.json`) is run through both implementations so a divergence
18
+ * fails a test rather than surfacing as "the SDK said this was valid and the API disagreed".
19
+ *
20
+ * Like the server's, this is a *basic* shape check by design — no email regex on an Email-variant
21
+ * input, for instance. It enforces the constraints a field definition actually declares, not
22
+ * stricter invented formats. Native browser validation still applies on top.
23
+ */
24
+ export declare function validateSubmission(form: SdkForm, data: SubmissionData, options: ValidateOptions): ValidationErrors;
25
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAExG,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,eAAe,GACvB,gBAAgB,CAoClB"}
@@ -0,0 +1,98 @@
1
+ import { collectDataFields } from './types.js';
2
+ /**
3
+ * A client-side port of the server's own `SubmissionDataValidator`, for immediate feedback before a
4
+ * step advances or a form submits.
5
+ *
6
+ * **The server remains authoritative.** These rules exist so a respondent sees a mistake without a
7
+ * round trip, not to be trusted — a client bypass still gets a real 400. They are kept deliberately
8
+ * identical to the server's, down to the message strings, and a shared fixture file
9
+ * (`src/test/fixtures/validation-cases.json`) is run through both implementations so a divergence
10
+ * fails a test rather than surfacing as "the SDK said this was valid and the API disagreed".
11
+ *
12
+ * Like the server's, this is a *basic* shape check by design — no email regex on an Email-variant
13
+ * input, for instance. It enforces the constraints a field definition actually declares, not
14
+ * stricter invented formats. Native browser validation still applies on top.
15
+ */
16
+ export function validateSubmission(form, data, options) {
17
+ const errors = {};
18
+ const fields = new Map(collectDataFields(form.steps.flatMap((step) => step.nodes))
19
+ .filter((field) => field.submissionKey !== undefined)
20
+ .map((field) => [field.submissionKey, field]));
21
+ // Unknown keys are errors, not ignored input — matching the server, which rejects them so
22
+ // arbitrary caller-supplied keys never accumulate in a column staff will later read.
23
+ for (const key of Object.keys(data)) {
24
+ if (!fields.has(key)) {
25
+ errors[key] = 'Unknown field.';
26
+ }
27
+ }
28
+ for (const [key, field] of fields) {
29
+ const value = data[key];
30
+ const present = value !== undefined && value !== null;
31
+ if (!present || isEmptyString(value)) {
32
+ if (options.requireAll && field.required) {
33
+ errors[key] = 'This field is required.';
34
+ }
35
+ continue;
36
+ }
37
+ const shapeError = validateShape(field, value);
38
+ if (shapeError !== null) {
39
+ errors[key] = shapeError;
40
+ }
41
+ }
42
+ return errors;
43
+ }
44
+ function isEmptyString(value) {
45
+ return typeof value === 'string' && value.trim() === '';
46
+ }
47
+ function validateShape(field, value) {
48
+ switch (field.fieldType) {
49
+ case 'Number':
50
+ return validateNumber(field.min ?? null, field.max ?? null, value);
51
+ case 'Choice':
52
+ return validateChoice(field.options.map((option) => option.value), value);
53
+ case 'Input':
54
+ case 'Textarea':
55
+ return typeof value === 'string' ? null : 'Value must be text.';
56
+ default:
57
+ return null;
58
+ }
59
+ }
60
+ function validateNumber(min, max, value) {
61
+ let parsed;
62
+ if (typeof value === 'number') {
63
+ parsed = value;
64
+ }
65
+ else if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
66
+ // A browser number input yields a string, so a numeric string is accepted rather than rejected
67
+ // on a technicality — same allowance the server makes.
68
+ parsed = Number(value);
69
+ }
70
+ else {
71
+ return 'Value must be a number.';
72
+ }
73
+ if (min !== null && parsed < min) {
74
+ return `Value must be ${min} or greater.`;
75
+ }
76
+ if (max !== null && parsed > max) {
77
+ return `Value must be ${max} or less.`;
78
+ }
79
+ return null;
80
+ }
81
+ function validateChoice(allowed, value) {
82
+ const permitted = new Set(allowed);
83
+ // A Checkbox choice submits several values; Select/Radio submit one. An array is accepted for any
84
+ // choice type, keeping this about membership rather than arity — which the model doesn't constrain.
85
+ if (Array.isArray(value)) {
86
+ for (const item of value) {
87
+ if (typeof item !== 'string' || !permitted.has(item)) {
88
+ return 'Value is not one of the allowed options.';
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ if (typeof value !== 'string' || !permitted.has(value)) {
94
+ return 'Value is not one of the allowed options.';
95
+ }
96
+ return null;
97
+ }
98
+ //# sourceMappingURL=validators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@ebitex/forms-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Fetch and render ebitex Forms in your own application.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./react": {
19
+ "types": "./dist/react/index.d.ts",
20
+ "import": "./dist/react/index.js"
21
+ },
22
+ "./styles.css": "./dist/react/styles.css"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
29
+ "lint": "oxlint",
30
+ "test": "vitest run"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=19",
34
+ "react-dom": ">=19"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "react": {
38
+ "optional": true
39
+ },
40
+ "react-dom": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@testing-library/jest-dom": "^7.0.0",
46
+ "@testing-library/react": "^16.3.2",
47
+ "@testing-library/user-event": "^14.6.1",
48
+ "@types/react": "^19.2.17",
49
+ "@types/react-dom": "^19.2.3",
50
+ "@vitejs/plugin-react": "^6.0.3",
51
+ "jsdom": "^29.1.1",
52
+ "oxlint": "^1.71.0",
53
+ "react": "^19.2.7",
54
+ "react-dom": "^19.2.7",
55
+ "typescript": "~6.0.2",
56
+ "vitest": "^4.1.10"
57
+ }
58
+ }