@usefillo/react 0.6.1 → 0.7.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.
- package/dist/index.d.ts +43 -1
- package/dist/index.js +51 -10
- package/dist/styles.css +33 -0
- package/dist/styles.unlayered.css +33 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentType, ReactNode, ReactElement } from 'react';
|
|
3
3
|
import * as _usefillo_core from '@usefillo/core';
|
|
4
|
-
import { FormSchema, FilloClient, ResponseData, FieldValue, FormPage, Block, FormStatus, FieldKind, Field, CustomField, FormTheme, FilloAppearance, FilloStrings, FilloError, CodeForm, FormSettings, Condition, TextField, PhoneField, NumberField, ChoiceField, SelectOption, CheckboxField, RatingField, LinearScaleField, RankingField, SignatureField, DateField, FileUploadField as FileUploadField$1, HiddenField } from '@usefillo/core';
|
|
4
|
+
import { FormSchema, FilloClient, ResponseData, FieldValue, FormPage, Block, FormStatus, FieldKind, Field, CustomField, FormTheme, FilloAppearance, FilloStrings, FilloRespondent, FilloError, CodeForm, FormSettings, Condition, TextField, PhoneField, NumberField, ChoiceField, SelectOption, CheckboxField, RatingField, LinearScaleField, RankingField, SignatureField, DateField, FileUploadField as FileUploadField$1, HiddenField } from '@usefillo/core';
|
|
5
5
|
export { CodeForm, Field, FieldValue, FileValue, FilloAppearance, FilloClient, FilloError, FilloJsxError, FilloSlot, FilloStrings, FormSchema, FormStatus, FormTheme, ProvisionWorkspaceResult, PublishedForm, ResponseData, SlotState, createClient, defineForm, provisionWorkspace, when } from '@usefillo/core';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -30,6 +30,32 @@ interface FilloApi {
|
|
|
30
30
|
uploading: boolean;
|
|
31
31
|
/** Human-readable message for the last failed submit; cleared on edit/retry. */
|
|
32
32
|
submitError?: string;
|
|
33
|
+
/**
|
|
34
|
+
* True when `status` is "submitted" because the once-per-visitor gate
|
|
35
|
+
* restored a previous visit's response, not because a submit happened in
|
|
36
|
+
* this mount. Skip one-time "just submitted" reactions (focus moves,
|
|
37
|
+
* redirects, confetti) when it's set — remounts replay them otherwise.
|
|
38
|
+
*/
|
|
39
|
+
restoredSubmission: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* True when a saved-progress draft (settings.saveProgress) restored answers
|
|
42
|
+
* and/or page position from a previous visit. Show a "picked up where you
|
|
43
|
+
* left off" affordance with a Start over action (resetDraft) when set.
|
|
44
|
+
*/
|
|
45
|
+
resumedDraft: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* True when an update-in-place limit prefilled the VERIFIED respondent's own
|
|
48
|
+
* previous answers — submitting updates that response in place.
|
|
49
|
+
*/
|
|
50
|
+
editingPrevious: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Persist unsaved draft progress right now (settings.saveProgress forms).
|
|
53
|
+
* The built-in renderers call it on pagehide/visibility-hidden; custom
|
|
54
|
+
* headless layouts inside <FilloProvider> get the same wiring for free.
|
|
55
|
+
*/
|
|
56
|
+
flushDraft: () => void;
|
|
57
|
+
/** Discard the saved draft and reset to a fresh fill ("Start over"). */
|
|
58
|
+
resetDraft: () => void;
|
|
33
59
|
/** Used by upload fields to gate submission. */
|
|
34
60
|
setUploading: (fieldId: string, busy: boolean) => void;
|
|
35
61
|
}
|
|
@@ -85,6 +111,14 @@ interface FilloFormBaseProps {
|
|
|
85
111
|
/** Renderers for your own `custom` field kinds, keyed by `component`. */
|
|
86
112
|
customComponents?: CustomComponents;
|
|
87
113
|
initialData?: ResponseData;
|
|
114
|
+
/**
|
|
115
|
+
* identify(): your app's account context for the person filling the form
|
|
116
|
+
* ({ id, email?, name?, traits? } — id is your own user id). Recorded with
|
|
117
|
+
* the response as an unverified claim so responses, webhooks, and
|
|
118
|
+
* integrations can say who answered. Safe to pass late (after your session
|
|
119
|
+
* loads).
|
|
120
|
+
*/
|
|
121
|
+
respondent?: FilloRespondent;
|
|
88
122
|
onChange?: (data: ResponseData) => void;
|
|
89
123
|
onSubmitted?: (responseId: string | undefined, data: ResponseData) => void;
|
|
90
124
|
/** Observe load and code-form sync failures (otherwise only logged). */
|
|
@@ -148,6 +182,7 @@ type ContentProps = {
|
|
|
148
182
|
id: string;
|
|
149
183
|
children?: string;
|
|
150
184
|
text?: string;
|
|
185
|
+
visibleIf?: Condition | Condition[];
|
|
151
186
|
};
|
|
152
187
|
/** Inert: rendering one throws; they exist to be read by the compiler. */
|
|
153
188
|
type Inert<P> = (props: P) => never;
|
|
@@ -194,6 +229,7 @@ declare const Fillo: {
|
|
|
194
229
|
readonly Paragraph: Inert<ContentProps>;
|
|
195
230
|
readonly Divider: Inert<{
|
|
196
231
|
id: string;
|
|
232
|
+
visibleIf?: Condition | Condition[];
|
|
197
233
|
}>;
|
|
198
234
|
readonly Page: Inert<{
|
|
199
235
|
id: string;
|
|
@@ -227,6 +263,12 @@ interface ControllerOptions {
|
|
|
227
263
|
surface?: "default" | "headless";
|
|
228
264
|
/** Resolve the submission target at submit time when formId is still unset. */
|
|
229
265
|
resolveFormId?: () => Promise<string>;
|
|
266
|
+
/**
|
|
267
|
+
* Host-app account context (identify()): who is filling this form, by your
|
|
268
|
+
* own user id. Recorded with the response as an unverified claim so the
|
|
269
|
+
* dashboard, webhooks, and integrations can say who answered.
|
|
270
|
+
*/
|
|
271
|
+
respondent?: FilloRespondent;
|
|
230
272
|
}
|
|
231
273
|
/**
|
|
232
274
|
* React binding for the framework-agnostic engine in @usefillo/core
|
package/dist/index.js
CHANGED
|
@@ -120,13 +120,19 @@ function useFilloController(options) {
|
|
|
120
120
|
skipValidation: options.skipValidation,
|
|
121
121
|
surface: options.surface ?? "headless",
|
|
122
122
|
// Wrapped so the engine (created once) always calls the latest resolver.
|
|
123
|
-
resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0
|
|
123
|
+
resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0,
|
|
124
|
+
respondent: options.respondent
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
127
|
const store = storeRef.current;
|
|
127
128
|
useEffect(() => {
|
|
128
|
-
store.setContext({
|
|
129
|
-
|
|
129
|
+
store.setContext({
|
|
130
|
+
form: options.form,
|
|
131
|
+
formId: options.formId,
|
|
132
|
+
client: options.client,
|
|
133
|
+
respondent: options.respondent
|
|
134
|
+
});
|
|
135
|
+
}, [store, options.form, options.formId, options.client, options.respondent]);
|
|
130
136
|
useEffect(() => () => store.destroy(), [store]);
|
|
131
137
|
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
132
138
|
return useMemo2(
|
|
@@ -139,11 +145,30 @@ function useFilloController(options) {
|
|
|
139
145
|
setUploading: store.setUploading,
|
|
140
146
|
next: store.next,
|
|
141
147
|
back: store.back,
|
|
142
|
-
submit: store.submit
|
|
148
|
+
submit: store.submit,
|
|
149
|
+
flushDraft: store.flushDraft,
|
|
150
|
+
resetDraft: store.resetDraft
|
|
143
151
|
}),
|
|
144
152
|
[store, state, options.form, options.formId, options.client]
|
|
145
153
|
);
|
|
146
154
|
}
|
|
155
|
+
function useDraftFlush(api) {
|
|
156
|
+
const enabled = Boolean(api.form.settings.saveProgress);
|
|
157
|
+
const flush = api.flushDraft;
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!enabled || typeof window === "undefined") return;
|
|
160
|
+
const onPageHide = () => flush();
|
|
161
|
+
const onVisibility = () => {
|
|
162
|
+
if (document.visibilityState === "hidden") flush();
|
|
163
|
+
};
|
|
164
|
+
window.addEventListener("pagehide", onPageHide);
|
|
165
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
166
|
+
return () => {
|
|
167
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
168
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
169
|
+
};
|
|
170
|
+
}, [enabled, flush]);
|
|
171
|
+
}
|
|
147
172
|
|
|
148
173
|
// src/define.ts
|
|
149
174
|
import {
|
|
@@ -1889,20 +1914,23 @@ function ResolvedForm(props) {
|
|
|
1889
1914
|
// Framed renderer: Fillo draws the layout (and badge), so the surface is
|
|
1890
1915
|
// "default", never the bare-engine "headless".
|
|
1891
1916
|
surface: "default",
|
|
1892
|
-
resolveFormId
|
|
1917
|
+
resolveFormId,
|
|
1918
|
+
respondent: props.respondent
|
|
1893
1919
|
});
|
|
1920
|
+
useDraftFlush(api);
|
|
1894
1921
|
const themed = useMemo5(() => themeProps(theme), [theme]);
|
|
1895
1922
|
const settings = schema.settings;
|
|
1896
1923
|
const submitted = api.status === "submitted";
|
|
1924
|
+
const liveSubmit = submitted && !api.restoredSubmission;
|
|
1897
1925
|
useEffect7(() => {
|
|
1898
|
-
if (!
|
|
1926
|
+
if (!liveSubmit || !settings.redirectUrl || typeof window === "undefined") return;
|
|
1899
1927
|
const url = safeHttpUrl(settings.redirectUrl);
|
|
1900
1928
|
if (url) window.location.assign(url);
|
|
1901
|
-
}, [
|
|
1929
|
+
}, [liveSubmit, settings.redirectUrl]);
|
|
1902
1930
|
const successRef = useRef5(null);
|
|
1903
1931
|
useEffect7(() => {
|
|
1904
|
-
if (
|
|
1905
|
-
}, [
|
|
1932
|
+
if (liveSubmit) successRef.current?.focus();
|
|
1933
|
+
}, [liveSubmit]);
|
|
1906
1934
|
const blocksRef = useRef5(null);
|
|
1907
1935
|
const prevPageIndex = useRef5(api.pageIndex);
|
|
1908
1936
|
useEffect7(() => {
|
|
@@ -1977,6 +2005,18 @@ function ResolvedForm(props) {
|
|
|
1977
2005
|
children: [
|
|
1978
2006
|
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
1979
2007
|
props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
|
|
2008
|
+
(api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs5(
|
|
2009
|
+
"div",
|
|
2010
|
+
{
|
|
2011
|
+
className: slotClass3("fillo-resume", appearance, { slot: "resume" }),
|
|
2012
|
+
"data-fillo": "resume",
|
|
2013
|
+
role: "status",
|
|
2014
|
+
children: [
|
|
2015
|
+
/* @__PURE__ */ jsx5("span", { className: "fillo-resume-text", children: api.editingPrevious ? strings.editNotice : strings.resumeNotice }),
|
|
2016
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: "fillo-resume-clear", onClick: api.resetDraft, children: strings.resumeStartOver })
|
|
2017
|
+
]
|
|
2018
|
+
}
|
|
2019
|
+
),
|
|
1980
2020
|
multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
|
|
1981
2021
|
"div",
|
|
1982
2022
|
{
|
|
@@ -1997,7 +2037,7 @@ function ResolvedForm(props) {
|
|
|
1997
2037
|
}
|
|
1998
2038
|
),
|
|
1999
2039
|
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
|
|
2000
|
-
/* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
|
|
2040
|
+
schema.title && /* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
|
|
2001
2041
|
schema.description && /* @__PURE__ */ jsx5(
|
|
2002
2042
|
"p",
|
|
2003
2043
|
{
|
|
@@ -2154,6 +2194,7 @@ function FilloProvider({ children, form, formId, client, appearance, strings, ..
|
|
|
2154
2194
|
// method; recorded per response so usage stays measurable.
|
|
2155
2195
|
surface: "headless"
|
|
2156
2196
|
});
|
|
2197
|
+
useDraftFlush(api);
|
|
2157
2198
|
return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolveStrings2(strings), children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
|
|
2158
2199
|
}
|
|
2159
2200
|
|
package/dist/styles.css
CHANGED
|
@@ -698,4 +698,37 @@
|
|
|
698
698
|
padding: 0.05em 0.3em;
|
|
699
699
|
border-radius: 4px;
|
|
700
700
|
}
|
|
701
|
+
|
|
702
|
+
/* ---------- Saved-progress resume notice ---------- */
|
|
703
|
+
.fillo-resume {
|
|
704
|
+
display: flex;
|
|
705
|
+
align-items: center;
|
|
706
|
+
justify-content: space-between;
|
|
707
|
+
gap: 0.75rem;
|
|
708
|
+
margin-bottom: 1rem;
|
|
709
|
+
padding: 0.6rem 0.9rem;
|
|
710
|
+
border: 1px solid var(--fillo-border);
|
|
711
|
+
border-radius: var(--fillo-radius);
|
|
712
|
+
background: var(--fillo-control-bg);
|
|
713
|
+
color: var(--fillo-muted);
|
|
714
|
+
font-size: 0.82rem;
|
|
715
|
+
line-height: 1.45;
|
|
716
|
+
}
|
|
717
|
+
.fillo-resume-clear {
|
|
718
|
+
border: 0;
|
|
719
|
+
background: none;
|
|
720
|
+
padding: 0;
|
|
721
|
+
color: var(--fillo-primary);
|
|
722
|
+
font: inherit;
|
|
723
|
+
font-weight: 500;
|
|
724
|
+
cursor: pointer;
|
|
725
|
+
text-decoration: underline;
|
|
726
|
+
text-underline-offset: 2px;
|
|
727
|
+
white-space: nowrap;
|
|
728
|
+
}
|
|
729
|
+
.fillo-resume-clear:focus-visible {
|
|
730
|
+
outline: none;
|
|
731
|
+
border-radius: 4px;
|
|
732
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
|
|
733
|
+
}
|
|
701
734
|
}
|
|
@@ -698,3 +698,36 @@
|
|
|
698
698
|
padding: 0.05em 0.3em;
|
|
699
699
|
border-radius: 4px;
|
|
700
700
|
}
|
|
701
|
+
|
|
702
|
+
/* ---------- Saved-progress resume notice ---------- */
|
|
703
|
+
.fillo-resume {
|
|
704
|
+
display: flex;
|
|
705
|
+
align-items: center;
|
|
706
|
+
justify-content: space-between;
|
|
707
|
+
gap: 0.75rem;
|
|
708
|
+
margin-bottom: 1rem;
|
|
709
|
+
padding: 0.6rem 0.9rem;
|
|
710
|
+
border: 1px solid var(--fillo-border);
|
|
711
|
+
border-radius: var(--fillo-radius);
|
|
712
|
+
background: var(--fillo-control-bg);
|
|
713
|
+
color: var(--fillo-muted);
|
|
714
|
+
font-size: 0.82rem;
|
|
715
|
+
line-height: 1.45;
|
|
716
|
+
}
|
|
717
|
+
.fillo-resume-clear {
|
|
718
|
+
border: 0;
|
|
719
|
+
background: none;
|
|
720
|
+
padding: 0;
|
|
721
|
+
color: var(--fillo-primary);
|
|
722
|
+
font: inherit;
|
|
723
|
+
font-weight: 500;
|
|
724
|
+
cursor: pointer;
|
|
725
|
+
text-decoration: underline;
|
|
726
|
+
text-underline-offset: 2px;
|
|
727
|
+
white-space: nowrap;
|
|
728
|
+
}
|
|
729
|
+
.fillo-resume-clear:focus-visible {
|
|
730
|
+
outline: none;
|
|
731
|
+
border-radius: 4px;
|
|
732
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
|
|
733
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefillo/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Headless React components for embedding Fillo forms natively in your product.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@usefillo/core": "^0.
|
|
36
|
+
"@usefillo/core": "^0.7.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^18.0.0 || ^19.0.0",
|