@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 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({ form: options.form, formId: options.formId, client: options.client });
129
- }, [store, options.form, options.formId, options.client]);
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 (!submitted || !settings.redirectUrl || typeof window === "undefined") return;
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
- }, [submitted, settings.redirectUrl]);
1929
+ }, [liveSubmit, settings.redirectUrl]);
1902
1930
  const successRef = useRef5(null);
1903
1931
  useEffect7(() => {
1904
- if (submitted) successRef.current?.focus();
1905
- }, [submitted]);
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.6.1",
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.6.1"
36
+ "@usefillo/core": "^0.7.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": "^18.0.0 || ^19.0.0",