@usefillo/react 0.20.0 → 0.21.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
@@ -23,6 +23,8 @@ interface FilloApi {
23
23
  isFirstPage: boolean;
24
24
  isLastPage: boolean;
25
25
  next: () => void;
26
+ /** Available on local preview controllers; a no-op unless explicitly enabled. */
27
+ skipPreviewPage?: () => void;
26
28
  back: () => void;
27
29
  submit: () => Promise<void>;
28
30
  status: FormStatus;
@@ -163,6 +165,8 @@ interface FilloFormBaseProps {
163
165
  uploadFileSizeLimitMb?: number;
164
166
  onChange?: (data: ResponseData) => void;
165
167
  onSubmitted?: (responseId: string | undefined, data: ResponseData) => void;
168
+ /** Observe a page change after the native renderer moves focus. Index is zero-based. */
169
+ onPageChange?: (pageIndex: number) => void;
166
170
  /** Observe load and code-form sync failures (otherwise only logged). */
167
171
  onError?: (error: FilloError) => void;
168
172
  /**
@@ -196,6 +200,12 @@ interface FilloFormBaseProps {
196
200
  * previews; real embeds need either `formId` or `defineForm()` + `client`.
197
201
  */
198
202
  renderOnly?: boolean;
203
+ /**
204
+ * Add a Skip action for browsing later pages without answering. Only enabled
205
+ * with explicit renderOnly. Native Next still validates and follows branches;
206
+ * Skip visits the next schema page, including pages outside the current route.
207
+ */
208
+ previewNavigation?: boolean;
199
209
  /**
200
210
  * @internal Builder/test preview: let "Next" move between pages without
201
211
  * client-side validation. Submission and branding are still authoritative.
@@ -209,6 +219,7 @@ type HostedFormProps = FilloFormBaseProps & {
209
219
  form?: undefined;
210
220
  client?: FilloClient;
211
221
  renderOnly?: false;
222
+ previewNavigation?: false;
212
223
  skipValidation?: false;
213
224
  };
214
225
  type InlineTargetedFormProps = FilloFormBaseProps & {
@@ -217,6 +228,7 @@ type InlineTargetedFormProps = FilloFormBaseProps & {
217
228
  formId: string;
218
229
  client?: FilloClient;
219
230
  renderOnly?: false;
231
+ previewNavigation?: false;
220
232
  skipValidation?: false;
221
233
  };
222
234
  type CodeBackedFormProps = FilloFormBaseProps & {
@@ -225,6 +237,7 @@ type CodeBackedFormProps = FilloFormBaseProps & {
225
237
  client: FilloClient;
226
238
  formId?: undefined;
227
239
  renderOnly?: false;
240
+ previewNavigation?: false;
228
241
  skipValidation?: false;
229
242
  };
230
243
  type RenderOnlyFormProps = FilloFormBaseProps & {
@@ -241,6 +254,7 @@ type LegacyPreviewFormProps = FilloFormBaseProps & {
241
254
  client?: FilloClient;
242
255
  formId?: string;
243
256
  renderOnly?: boolean;
257
+ previewNavigation?: false;
244
258
  skipValidation: true;
245
259
  };
246
260
  type FilloFormProps = HostedFormProps | InlineTargetedFormProps | CodeBackedFormProps | RenderOnlyFormProps | LegacyPreviewFormProps;
@@ -269,7 +283,7 @@ type ContentProps = {
269
283
  };
270
284
  /** Inert: rendering one throws; they exist to be read by the compiler. */
271
285
  type Inert<P> = (props: P) => never;
272
- interface FilloJsxFormBaseProps extends Omit<FilloFormProps, "form" | "formId" | "client" | "renderOnly" | "skipValidation"> {
286
+ interface FilloJsxFormBaseProps extends Omit<FilloFormProps, "form" | "formId" | "client" | "renderOnly" | "skipValidation" | "previewNavigation"> {
273
287
  /** Project handle — the form's identity across syncs. */
274
288
  id: string;
275
289
  title?: string;
@@ -282,14 +296,17 @@ type FilloJsxFormProps = FilloJsxFormBaseProps & ({
282
296
  client: FilloClient;
283
297
  renderOnly?: false;
284
298
  skipValidation?: false;
299
+ previewNavigation?: false;
285
300
  } | {
286
301
  client?: FilloClient;
287
302
  renderOnly: true;
288
303
  skipValidation?: boolean;
304
+ previewNavigation?: boolean;
289
305
  } | {
290
306
  client?: FilloClient;
291
307
  renderOnly?: boolean;
292
308
  skipValidation: true;
309
+ previewNavigation?: false;
293
310
  });
294
311
  declare function JsxForm(props: FilloJsxFormProps): react.JSX.Element;
295
312
  declare function defineFormFromJsx(element: ReactElement<FilloJsxFormProps>): CodeForm;
@@ -351,6 +368,8 @@ interface ControllerOptions {
351
368
  getHoneypot?: () => string;
352
369
  /** @internal Preview-only page navigation escape hatch. Submission still validates. */
353
370
  skipValidation?: boolean;
371
+ /** Enable the explicit Skip action in a transportless local preview. */
372
+ previewNavigation?: boolean;
354
373
  /**
355
374
  * Embedding surface, recorded per response for measurement. Defaults to
356
375
  * "headless" (your own markup), matching @usefillo/core — the framed
package/dist/index.js CHANGED
@@ -249,6 +249,7 @@ function useFilloController(options) {
249
249
  onSubmitted: (id, d) => optsRef.current.onSubmitted?.(id, d),
250
250
  getHoneypot: () => optsRef.current.getHoneypot?.() ?? "",
251
251
  skipValidation: options.skipValidation,
252
+ previewNavigation: options.previewNavigation,
252
253
  surface: options.surface ?? "headless",
253
254
  // Wrapped so the engine (created once) always calls the latest resolver.
254
255
  resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0,
@@ -295,6 +296,7 @@ function useFilloController(options) {
295
296
  setValue: store.setValue,
296
297
  setUploading: store.setUploading,
297
298
  next: store.next,
299
+ skipPreviewPage: store.skipPreviewPage,
298
300
  back: store.back,
299
301
  submit: store.submit,
300
302
  flushDraft: store.flushDraft,
@@ -1781,6 +1783,25 @@ function ChoiceIcon({ icon }) {
1781
1783
  }
1782
1784
  );
1783
1785
  }
1786
+ function SelectionCheck() {
1787
+ return /* @__PURE__ */ jsx6(
1788
+ "svg",
1789
+ {
1790
+ className: "fillo-option-check",
1791
+ width: "1em",
1792
+ height: "1em",
1793
+ viewBox: "0 0 24 24",
1794
+ fill: "none",
1795
+ stroke: "currentColor",
1796
+ strokeWidth: "2",
1797
+ strokeLinecap: "square",
1798
+ strokeLinejoin: "miter",
1799
+ "aria-hidden": "true",
1800
+ focusable: "false",
1801
+ children: /* @__PURE__ */ jsx6("path", { d: "M4 12l5 5L20 6" })
1802
+ }
1803
+ );
1804
+ }
1784
1805
  function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1785
1806
  const ids = useFilloFieldIds(field.id, providedIds);
1786
1807
  const appearance = useFilloAppearance();
@@ -1826,10 +1847,10 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
1826
1847
  }
1827
1848
  ),
1828
1849
  /* @__PURE__ */ jsx6(ChoiceIcon, { icon: opt.icon }),
1850
+ /* @__PURE__ */ jsx6("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label }),
1829
1851
  opt.icon && value === opt.id && // Icon-mode hides the native input, so its check state is
1830
1852
  // otherwise color-only (forfeits forced-colors' own indicator).
1831
- /* @__PURE__ */ jsx6("span", { className: "fillo-option-check", "aria-hidden": "true", children: "\u2713" }),
1832
- /* @__PURE__ */ jsx6("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1853
+ /* @__PURE__ */ jsx6(SelectionCheck, {})
1833
1854
  ]
1834
1855
  },
1835
1856
  opt.id
@@ -1929,10 +1950,10 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
1929
1950
  }
1930
1951
  ),
1931
1952
  /* @__PURE__ */ jsx6(ChoiceIcon, { icon: opt.icon }),
1953
+ /* @__PURE__ */ jsx6("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label }),
1932
1954
  opt.icon && selected.includes(opt.id) && // Icon-mode hides the native input, so its check state is
1933
1955
  // otherwise color-only (forfeits forced-colors' own indicator).
1934
- /* @__PURE__ */ jsx6("span", { className: "fillo-option-check", "aria-hidden": "true", children: "\u2713" }),
1935
- /* @__PURE__ */ jsx6("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
1956
+ /* @__PURE__ */ jsx6(SelectionCheck, {})
1936
1957
  ]
1937
1958
  },
1938
1959
  opt.id
@@ -3084,6 +3105,7 @@ function FilloForm(props) {
3084
3105
  theme,
3085
3106
  formId: renderOnly ? void 0 : syncedFormId ?? formId,
3086
3107
  renderOnly,
3108
+ previewNavigation: props.renderOnly === true && props.previewNavigation === true,
3087
3109
  skipValidation: renderOnly ? true : props.skipValidation,
3088
3110
  poweredBy,
3089
3111
  brandLabel: branding?.label,
@@ -3166,6 +3188,7 @@ function ResolvedForm(props) {
3166
3188
  const { schema, theme } = props;
3167
3189
  const displayOnly = props.displayOnly === true;
3168
3190
  const renderOnly = props.renderOnly === true;
3191
+ const previewNavigation = renderOnly && !displayOnly && props.previewNavigation === true;
3169
3192
  const instanceId = useId2().replace(/:/g, "");
3170
3193
  const devEnv = useIsDevEnv();
3171
3194
  const devChrome = !displayOnly && (props.preview === true || devEnv);
@@ -3269,6 +3292,7 @@ function ResolvedForm(props) {
3269
3292
  onChange: props.onChange,
3270
3293
  onSubmitted: props.onSubmitted,
3271
3294
  skipValidation: props.skipValidation,
3295
+ previewNavigation,
3272
3296
  getHoneypot: () => hpRef.current?.value ?? "",
3273
3297
  // Framed renderer: Fillo draws the layout (and badge), so the surface is
3274
3298
  // "default", never the bare-engine "headless".
@@ -3337,10 +3361,10 @@ function ResolvedForm(props) {
3337
3361
  const submitted = api.status === "submitted";
3338
3362
  const liveSubmit = submitted && !api.restoredSubmission;
3339
3363
  useEffect9(() => {
3340
- if (!liveSubmit || !settings.redirectUrl || typeof window === "undefined") return;
3364
+ if (renderOnly || !liveSubmit || !settings.redirectUrl || typeof window === "undefined") return;
3341
3365
  const url = safeHttpUrl(settings.redirectUrl);
3342
3366
  if (url) window.location.assign(url);
3343
- }, [liveSubmit, settings.redirectUrl]);
3367
+ }, [liveSubmit, settings.redirectUrl, renderOnly]);
3344
3368
  const successRef = useRef9(null);
3345
3369
  useEffect9(() => {
3346
3370
  if (liveSubmit) successRef.current?.focus();
@@ -3353,10 +3377,12 @@ function ResolvedForm(props) {
3353
3377
  const container = blocksRef.current?.parentElement;
3354
3378
  const heading = container?.querySelector(".fillo-page-title, .fillo-title");
3355
3379
  const target = heading ?? blocksRef.current;
3356
- if (!target) return;
3357
- if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1");
3358
- target.focus();
3359
- }, [api.pageIndex]);
3380
+ if (target) {
3381
+ if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", "-1");
3382
+ target.focus();
3383
+ }
3384
+ props.onPageChange?.(api.pageIndex);
3385
+ }, [api.pageIndex, props.onPageChange]);
3360
3386
  const appearance = props.appearance;
3361
3387
  if (api.status === "submitted") {
3362
3388
  return /* @__PURE__ */ jsxs7(
@@ -3399,11 +3425,10 @@ function ResolvedForm(props) {
3399
3425
  const multiPage = api.pageCount > 1;
3400
3426
  const autoSubmit = settings.submitMode === "auto";
3401
3427
  const showChallenge = challengeRequired && api.isLastPage;
3402
- const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(reachableFields(api.form, api.data)) || // A challenge needs an explicit Submit even on an otherwise button-less
3403
- // auto-submit form — you can't one-tap past a human check.
3404
- showChallenge;
3405
- const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
3406
- const progressSeq = reachablePageSequence(api.form, api.data);
3428
+ const showPrimaryButton = previewNavigation || !autoSubmit || !api.isLastPage || needsExplicitSubmit(reachableFields(api.form, api.data)) || showChallenge;
3429
+ const showPreviewSkip = previewNavigation && api.pageIndex < api.pageCount - 1;
3430
+ const showFooter = showPrimaryButton || multiPage && !api.isFirstPage || showPreviewSkip;
3431
+ const progressSeq = previewNavigation ? schema.pages.map((page) => page.id) : reachablePageSequence(api.form, api.data);
3407
3432
  const progressTotal = Math.max(progressSeq.length, 1);
3408
3433
  const progressPos = progressSeq.indexOf(api.page.id);
3409
3434
  const progressStep = progressPos >= 0 ? progressPos + 1 : Math.min(api.pageIndex + 1, progressTotal);
@@ -3594,6 +3619,21 @@ function ResolvedForm(props) {
3594
3619
  children: strings.back
3595
3620
  }
3596
3621
  ),
3622
+ showPreviewSkip && /* @__PURE__ */ jsx8(
3623
+ "button",
3624
+ {
3625
+ type: "button",
3626
+ className: slotClass3("fillo-button fillo-button--ghost", appearance, {
3627
+ slot: "button",
3628
+ variant: "ghost"
3629
+ }),
3630
+ "data-fillo": "button",
3631
+ "data-fillo-preview-skip": "",
3632
+ onClick: api.skipPreviewPage,
3633
+ disabled: api.status === "submitting",
3634
+ children: strings.previewSkip
3635
+ }
3636
+ ),
3597
3637
  showPrimaryButton && /* @__PURE__ */ jsx8(
3598
3638
  "button",
3599
3639
  {
@@ -3603,10 +3643,7 @@ function ResolvedForm(props) {
3603
3643
  variant: "primary"
3604
3644
  }),
3605
3645
  "data-fillo": "button",
3606
- disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
3607
- // "Next"). The server rejects a tokenless submit regardless —
3608
- // this is UX only.
3609
- showChallenge && !challengeToken,
3646
+ disabled: api.status === "submitting" || api.uploading || showChallenge && !challengeToken,
3610
3647
  children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
3611
3648
  }
3612
3649
  )
@@ -3615,20 +3652,15 @@ function ResolvedForm(props) {
3615
3652
  ),
3616
3653
  props.poweredBy && /* @__PURE__ */ jsx8(PoweredBy, { label: props.brandLabel, href: props.brandHref })
3617
3654
  ] });
3618
- return /* @__PURE__ */ jsx8(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx8(FilloChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx8(FilloAnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx8(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx8(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx8(FilloInstanceIdContext.Provider, { value: instanceId, children: displayOnly ? (
3619
- // Display-only preview: a plain div (no form element to submit) with
3620
- // every control inside a natively disabled fieldset — unfocusable
3621
- // and unfillable even without the stylesheet or inert support.
3622
- /* @__PURE__ */ jsx8(
3623
- "div",
3624
- {
3625
- className: rootClass,
3626
- "data-fillo": "preview-form",
3627
- "data-state": "preview",
3628
- ...themed,
3629
- children: /* @__PURE__ */ jsx8("fieldset", { disabled: true, className: "fillo-not-open-fields", children: formBody })
3630
- }
3631
- )
3655
+ return /* @__PURE__ */ jsx8(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx8(FilloChromeContext.Provider, { value: chromeValue, children: /* @__PURE__ */ jsx8(FilloAnnounceContext.Provider, { value: announce, children: /* @__PURE__ */ jsx8(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx8(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx8(FilloInstanceIdContext.Provider, { value: instanceId, children: displayOnly ? /* @__PURE__ */ jsx8(
3656
+ "div",
3657
+ {
3658
+ className: rootClass,
3659
+ "data-fillo": "preview-form",
3660
+ "data-state": "preview",
3661
+ ...themed,
3662
+ children: /* @__PURE__ */ jsx8("fieldset", { disabled: true, className: "fillo-not-open-fields", children: formBody })
3663
+ }
3632
3664
  ) : /* @__PURE__ */ jsx8(
3633
3665
  "form",
3634
3666
  {
package/dist/styles.css CHANGED
@@ -93,22 +93,25 @@
93
93
  }
94
94
 
95
95
  .fillo-header { display: flex; flex-direction: column; gap: 0.5rem; }
96
- .fillo-title { font-size: 1.6em; font-weight: 650; letter-spacing: 0; margin: 0; }
96
+ /* Text roles: title 1.5em, heading 1.125em, body 1em, helper 0.875em,
97
+ metadata 0.75em. Nested helper text inherits its container size.
98
+ Glyphs used as icons (stars, flags, carets) have independent sizes. */
99
+ .fillo-title { font-size: 1.5em; font-weight: 600; letter-spacing: 0; margin: 0; }
97
100
  .fillo-form-description { color: var(--fillo-muted); margin: 0; line-height: 1.55; }
98
- .fillo-page-title { font-size: 1.2em; font-weight: 600; margin: 0; }
101
+ .fillo-page-title { font-size: 1.125em; font-weight: 600; margin: 0; }
99
102
 
100
103
  .fillo-blocks { display: flex; flex-direction: column; gap: 1.5rem; }
101
104
 
102
105
  /* ---------- Field chrome ---------- */
103
106
  .fillo-field { display: flex; flex-direction: column; gap: 0.45rem; }
104
- .fillo-label { font-weight: 550; font-size: 0.95em; }
107
+ .fillo-label { font-weight: 500; font-size: 1em; }
105
108
  :where(.fillo-field--error) .fillo-label { color: var(--fillo-error); }
106
109
  /* Fillo marks the few optional fields rather than starring the many required
107
110
  ones — so a clean label reads as required by default. */
108
- .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.85em; }
109
- .fillo-description { color: var(--fillo-muted); font-size: 0.85em; margin: 0; line-height: 1.5; }
110
- .fillo-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; line-height: 1.4; margin: 0; }
111
- .fillo-submit-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; margin: 0; }
111
+ .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.875em; }
112
+ .fillo-description { color: var(--fillo-muted); font-size: 0.875em; margin: 0; line-height: 1.5; }
113
+ .fillo-error { color: var(--fillo-error); font-size: 0.875em; font-weight: 500; line-height: 1.4; margin: 0; }
114
+ .fillo-submit-error { color: var(--fillo-error); font-size: 0.875em; font-weight: 500; margin: 0; }
112
115
 
113
116
  /* ---------- Inputs ---------- */
114
117
  .fillo-input {
@@ -326,7 +329,7 @@
326
329
  gap: 0.55rem;
327
330
  }
328
331
  :where(.fillo-option--with-other) .fillo-option-label {
329
- font-weight: 550;
332
+ font-weight: 500;
330
333
  }
331
334
 
332
335
  /* ---------- Toggle checkbox ---------- */
@@ -447,7 +450,7 @@
447
450
  min-width: 0;
448
451
  padding: 0.5rem 0.15rem;
449
452
  font: inherit;
450
- font-size: 0.92em;
453
+ font-size: 1em;
451
454
  font-variant-numeric: tabular-nums;
452
455
  text-align: center;
453
456
  border: 1px solid var(--fillo-border);
@@ -476,7 +479,7 @@
476
479
  display: flex;
477
480
  justify-content: space-between;
478
481
  color: var(--fillo-muted);
479
- font-size: 0.78em;
482
+ font-size: 0.875em;
480
483
  margin-top: 0.35rem;
481
484
  }
482
485
 
@@ -499,8 +502,8 @@
499
502
  border-radius: 999px;
500
503
  background: color-mix(in srgb, var(--fillo-primary) 10%, transparent);
501
504
  color: var(--fillo-primary);
502
- font-size: 0.78em;
503
- font-weight: 650;
505
+ font-size: 0.875em;
506
+ font-weight: 600;
504
507
  font-variant-numeric: tabular-nums;
505
508
  }
506
509
  .fillo-ranking-label { flex: 1; font-size: 1em; }
@@ -522,13 +525,13 @@
522
525
 
523
526
  /* ---------- Matrix ---------- */
524
527
  .fillo-matrix-wrap { overflow-x: auto; }
525
- .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 0.9em; }
528
+ .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 1em; }
526
529
  :where(.fillo-matrix) th, :where(.fillo-matrix) td {
527
530
  padding: 0.55rem 0.7rem;
528
531
  text-align: center;
529
532
  border-bottom: 1px solid var(--fillo-border);
530
533
  }
531
- .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.89em; }
534
+ .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.875em; }
532
535
  .fillo-matrix tbody th { text-align: start; font-weight: 500; }
533
536
  .fillo-matrix tbody tr:last-child th, .fillo-matrix tbody tr:last-child td { border-bottom: none; }
534
537
  .fillo-matrix-cell:focus-within {
@@ -542,7 +545,7 @@
542
545
  /* A quiet computed-value line, not a control: no border or input chrome —
543
546
  the slightly heavier tabular number is the only emphasis. */
544
547
  .fillo-calculated-value {
545
- font-size: 1.05em;
548
+ font-size: 1em;
546
549
  font-weight: 600;
547
550
  font-variant-numeric: tabular-nums;
548
551
  color: var(--fillo-text);
@@ -571,7 +574,7 @@
571
574
  display: grid;
572
575
  place-items: center;
573
576
  color: var(--fillo-muted);
574
- font-size: 0.85em;
577
+ font-size: 0.875em;
575
578
  pointer-events: none;
576
579
  }
577
580
  .fillo-signature-clear {
@@ -579,7 +582,7 @@
579
582
  top: 0.4rem;
580
583
  inset-inline-end: 0.4rem;
581
584
  font: inherit;
582
- font-size: 0.75em;
585
+ font-size: 0.875em;
583
586
  border: 1px solid var(--fillo-border);
584
587
  border-radius: calc(var(--fillo-radius) * 0.6);
585
588
  background: var(--fillo-control-bg);
@@ -598,7 +601,7 @@
598
601
  }
599
602
  .fillo-signature-type-label {
600
603
  color: var(--fillo-muted);
601
- font-size: 0.8em;
604
+ font-size: 0.875em;
602
605
  }
603
606
  .fillo-signature-type-input {
604
607
  margin-top: 0;
@@ -638,8 +641,8 @@
638
641
  outline: none;
639
642
  }
640
643
  .fillo-dropzone--disabled { cursor: not-allowed; opacity: 0.6; }
641
- .fillo-dropzone-title { font-weight: 550; font-size: 0.95em; }
642
- .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82em; }
644
+ .fillo-dropzone-title { font-weight: 500; font-size: 1em; }
645
+ .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.875em; }
643
646
 
644
647
  .fillo-files { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; margin: 0; padding: 0; }
645
648
  .fillo-file {
@@ -650,7 +653,7 @@
650
653
  border: 1px solid var(--fillo-border);
651
654
  border-radius: var(--fillo-radius);
652
655
  padding: 0.625rem 0.75rem;
653
- font-size: 0.88em;
656
+ font-size: 1em;
654
657
  background: var(--fillo-control-bg);
655
658
  box-sizing: border-box;
656
659
  min-width: 0;
@@ -710,7 +713,7 @@
710
713
  }
711
714
  .fillo-file-error {
712
715
  color: var(--fillo-error);
713
- font-size: 0.93em;
716
+ font-size: 0.875em;
714
717
  line-height: 1.35;
715
718
  overflow-wrap: anywhere;
716
719
  }
@@ -745,7 +748,7 @@
745
748
  cursor: pointer;
746
749
  color: var(--fillo-primary);
747
750
  font: inherit;
748
- font-weight: 550;
751
+ font-weight: 500;
749
752
  padding: 0 0.5rem;
750
753
  }
751
754
  .fillo-file-retry:hover { text-decoration: underline; }
@@ -765,7 +768,7 @@
765
768
  }
766
769
 
767
770
  /* ---------- Content blocks ---------- */
768
- .fillo-heading { font-size: 1.15em; font-weight: 600; margin: 0.5rem 0 0; }
771
+ .fillo-heading { font-size: 1.125em; font-weight: 600; margin: 0.5rem 0 0; }
769
772
  .fillo-paragraph { color: var(--fillo-muted); line-height: 1.6; margin: 0; }
770
773
  .fillo-divider { border: none; border-top: 1px solid var(--fillo-border); margin: 0.25rem 0; }
771
774
 
@@ -773,7 +776,7 @@
773
776
  .fillo-footer { display: flex; justify-content: flex-end; gap: 0.6rem; }
774
777
  .fillo-button {
775
778
  font: inherit;
776
- font-weight: 550;
779
+ font-weight: 500;
777
780
  border-radius: var(--fillo-radius);
778
781
  padding: 0.6rem 1.4rem;
779
782
  cursor: pointer;
@@ -839,7 +842,7 @@
839
842
  padding: 2rem 1rem;
840
843
  text-align: center;
841
844
  }
842
- .fillo-not-open-title { margin: 0; font-size: 1.05em; font-weight: 650; letter-spacing: 0; }
845
+ .fillo-not-open-title { margin: 0; font-size: 1.125em; font-weight: 600; letter-spacing: 0; }
843
846
  .fillo-not-open-body {
844
847
  max-width: 26rem;
845
848
  margin: 0;
@@ -867,7 +870,7 @@
867
870
  border-bottom: 2.5px solid var(--fillo-primary-contrast);
868
871
  transform: translateY(-0.09rem) rotate(-45deg);
869
872
  }
870
- .fillo-success-title { margin: 0; font-size: 1.05em; letter-spacing: 0; }
873
+ .fillo-success-title { margin: 0; font-weight: 600; font-size: 1.125em; letter-spacing: 0; }
871
874
  .fillo-success-message { margin: 0; font-size: 0.875em; color: var(--fillo-muted); }
872
875
 
873
876
  /* Honeypot — off-screen, never display:none (bots check that) */
@@ -947,12 +950,12 @@
947
950
  border-radius: var(--fillo-radius);
948
951
  background: #fffbeb;
949
952
  color: #92400e;
950
- font-size: 0.82em;
953
+ font-size: 0.875em;
951
954
  line-height: 1.45;
952
955
  }
953
956
  .fillo-devwarning code {
954
957
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
955
- font-size: 0.92em;
958
+ font-size: 1em;
956
959
  background: #fef3c7;
957
960
  padding: 0.05em 0.3em;
958
961
  border-radius: 4px;
@@ -966,7 +969,7 @@
966
969
  border-radius: 999px;
967
970
  background: #fef3c7;
968
971
  color: #92400e;
969
- font-size: 0.7em;
972
+ font-size: 0.75em;
970
973
  font-weight: 600;
971
974
  letter-spacing: 0.06em;
972
975
  text-transform: uppercase;
@@ -984,7 +987,7 @@
984
987
  border-radius: var(--fillo-radius);
985
988
  background: var(--fillo-control-bg);
986
989
  color: var(--fillo-muted);
987
- font-size: 0.82em;
990
+ font-size: 0.875em;
988
991
  line-height: 1.45;
989
992
  }
990
993
  .fillo-resume-clear {
@@ -1024,7 +1027,7 @@
1024
1027
  .fillo-turnstile-error {
1025
1028
  margin: 0;
1026
1029
  color: var(--fillo-error);
1027
- font-size: 0.82em;
1030
+ font-size: 0.875em;
1028
1031
  line-height: 1.45;
1029
1032
  }
1030
1033
 
@@ -1045,7 +1048,7 @@
1045
1048
  justify-content: space-between;
1046
1049
  gap: 0.5rem;
1047
1050
  }
1048
- .fillo-group-instance-title { margin: 0; font-size: 0.95em; font-weight: 600; }
1051
+ .fillo-group-instance-title { margin: 0; font-size: 1.125em; font-weight: 600; }
1049
1052
  .fillo-group-add {
1050
1053
  align-self: flex-start;
1051
1054
  font: inherit;
@@ -1133,7 +1136,7 @@
1133
1136
  .fillo-matrix tbody th { padding: 0.55rem 0 0.15rem; }
1134
1137
  .fillo-matrix td { display: flex; align-items: center; gap: 0.5rem; }
1135
1138
  .fillo-matrix td::before { content: attr(data-label);
1136
- flex: 1 1 auto; font-size: 0.85em; }
1139
+ flex: 1 1 auto; font-size: 0.875em; }
1137
1140
  /* Deviates from the spec's literal ".fillo-matrix .fillo-matrix-cell"
1138
1141
  text (two bare fillo-* classes, no :where()) — that fails this file's
1139
1142
  own specificity lint (styles-lint.test.mjs: "wrap qualifiers in
@@ -93,22 +93,25 @@
93
93
  }
94
94
 
95
95
  .fillo-header { display: flex; flex-direction: column; gap: 0.5rem; }
96
- .fillo-title { font-size: 1.6em; font-weight: 650; letter-spacing: 0; margin: 0; }
96
+ /* Text roles: title 1.5em, heading 1.125em, body 1em, helper 0.875em,
97
+ metadata 0.75em. Nested helper text inherits its container size.
98
+ Glyphs used as icons (stars, flags, carets) have independent sizes. */
99
+ .fillo-title { font-size: 1.5em; font-weight: 600; letter-spacing: 0; margin: 0; }
97
100
  .fillo-form-description { color: var(--fillo-muted); margin: 0; line-height: 1.55; }
98
- .fillo-page-title { font-size: 1.2em; font-weight: 600; margin: 0; }
101
+ .fillo-page-title { font-size: 1.125em; font-weight: 600; margin: 0; }
99
102
 
100
103
  .fillo-blocks { display: flex; flex-direction: column; gap: 1.5rem; }
101
104
 
102
105
  /* ---------- Field chrome ---------- */
103
106
  .fillo-field { display: flex; flex-direction: column; gap: 0.45rem; }
104
- .fillo-label { font-weight: 550; font-size: 0.95em; }
107
+ .fillo-label { font-weight: 500; font-size: 1em; }
105
108
  :where(.fillo-field--error) .fillo-label { color: var(--fillo-error); }
106
109
  /* Fillo marks the few optional fields rather than starring the many required
107
110
  ones — so a clean label reads as required by default. */
108
- .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.85em; }
109
- .fillo-description { color: var(--fillo-muted); font-size: 0.85em; margin: 0; line-height: 1.5; }
110
- .fillo-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; line-height: 1.4; margin: 0; }
111
- .fillo-submit-error { color: var(--fillo-error); font-size: 0.85em; font-weight: 500; margin: 0; }
111
+ .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.875em; }
112
+ .fillo-description { color: var(--fillo-muted); font-size: 0.875em; margin: 0; line-height: 1.5; }
113
+ .fillo-error { color: var(--fillo-error); font-size: 0.875em; font-weight: 500; line-height: 1.4; margin: 0; }
114
+ .fillo-submit-error { color: var(--fillo-error); font-size: 0.875em; font-weight: 500; margin: 0; }
112
115
 
113
116
  /* ---------- Inputs ---------- */
114
117
  .fillo-input {
@@ -326,7 +329,7 @@
326
329
  gap: 0.55rem;
327
330
  }
328
331
  :where(.fillo-option--with-other) .fillo-option-label {
329
- font-weight: 550;
332
+ font-weight: 500;
330
333
  }
331
334
 
332
335
  /* ---------- Toggle checkbox ---------- */
@@ -447,7 +450,7 @@
447
450
  min-width: 0;
448
451
  padding: 0.5rem 0.15rem;
449
452
  font: inherit;
450
- font-size: 0.92em;
453
+ font-size: 1em;
451
454
  font-variant-numeric: tabular-nums;
452
455
  text-align: center;
453
456
  border: 1px solid var(--fillo-border);
@@ -476,7 +479,7 @@
476
479
  display: flex;
477
480
  justify-content: space-between;
478
481
  color: var(--fillo-muted);
479
- font-size: 0.78em;
482
+ font-size: 0.875em;
480
483
  margin-top: 0.35rem;
481
484
  }
482
485
 
@@ -499,8 +502,8 @@
499
502
  border-radius: 999px;
500
503
  background: color-mix(in srgb, var(--fillo-primary) 10%, transparent);
501
504
  color: var(--fillo-primary);
502
- font-size: 0.78em;
503
- font-weight: 650;
505
+ font-size: 0.875em;
506
+ font-weight: 600;
504
507
  font-variant-numeric: tabular-nums;
505
508
  }
506
509
  .fillo-ranking-label { flex: 1; font-size: 1em; }
@@ -522,13 +525,13 @@
522
525
 
523
526
  /* ---------- Matrix ---------- */
524
527
  .fillo-matrix-wrap { overflow-x: auto; }
525
- .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 0.9em; }
528
+ .fillo-matrix { border-collapse: collapse; width: 100%; font-size: 1em; }
526
529
  :where(.fillo-matrix) th, :where(.fillo-matrix) td {
527
530
  padding: 0.55rem 0.7rem;
528
531
  text-align: center;
529
532
  border-bottom: 1px solid var(--fillo-border);
530
533
  }
531
- .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.89em; }
534
+ .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.875em; }
532
535
  .fillo-matrix tbody th { text-align: start; font-weight: 500; }
533
536
  .fillo-matrix tbody tr:last-child th, .fillo-matrix tbody tr:last-child td { border-bottom: none; }
534
537
  .fillo-matrix-cell:focus-within {
@@ -542,7 +545,7 @@
542
545
  /* A quiet computed-value line, not a control: no border or input chrome —
543
546
  the slightly heavier tabular number is the only emphasis. */
544
547
  .fillo-calculated-value {
545
- font-size: 1.05em;
548
+ font-size: 1em;
546
549
  font-weight: 600;
547
550
  font-variant-numeric: tabular-nums;
548
551
  color: var(--fillo-text);
@@ -571,7 +574,7 @@
571
574
  display: grid;
572
575
  place-items: center;
573
576
  color: var(--fillo-muted);
574
- font-size: 0.85em;
577
+ font-size: 0.875em;
575
578
  pointer-events: none;
576
579
  }
577
580
  .fillo-signature-clear {
@@ -579,7 +582,7 @@
579
582
  top: 0.4rem;
580
583
  inset-inline-end: 0.4rem;
581
584
  font: inherit;
582
- font-size: 0.75em;
585
+ font-size: 0.875em;
583
586
  border: 1px solid var(--fillo-border);
584
587
  border-radius: calc(var(--fillo-radius) * 0.6);
585
588
  background: var(--fillo-control-bg);
@@ -598,7 +601,7 @@
598
601
  }
599
602
  .fillo-signature-type-label {
600
603
  color: var(--fillo-muted);
601
- font-size: 0.8em;
604
+ font-size: 0.875em;
602
605
  }
603
606
  .fillo-signature-type-input {
604
607
  margin-top: 0;
@@ -638,8 +641,8 @@
638
641
  outline: none;
639
642
  }
640
643
  .fillo-dropzone--disabled { cursor: not-allowed; opacity: 0.6; }
641
- .fillo-dropzone-title { font-weight: 550; font-size: 0.95em; }
642
- .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82em; }
644
+ .fillo-dropzone-title { font-weight: 500; font-size: 1em; }
645
+ .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.875em; }
643
646
 
644
647
  .fillo-files { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; margin: 0; padding: 0; }
645
648
  .fillo-file {
@@ -650,7 +653,7 @@
650
653
  border: 1px solid var(--fillo-border);
651
654
  border-radius: var(--fillo-radius);
652
655
  padding: 0.625rem 0.75rem;
653
- font-size: 0.88em;
656
+ font-size: 1em;
654
657
  background: var(--fillo-control-bg);
655
658
  box-sizing: border-box;
656
659
  min-width: 0;
@@ -710,7 +713,7 @@
710
713
  }
711
714
  .fillo-file-error {
712
715
  color: var(--fillo-error);
713
- font-size: 0.93em;
716
+ font-size: 0.875em;
714
717
  line-height: 1.35;
715
718
  overflow-wrap: anywhere;
716
719
  }
@@ -745,7 +748,7 @@
745
748
  cursor: pointer;
746
749
  color: var(--fillo-primary);
747
750
  font: inherit;
748
- font-weight: 550;
751
+ font-weight: 500;
749
752
  padding: 0 0.5rem;
750
753
  }
751
754
  .fillo-file-retry:hover { text-decoration: underline; }
@@ -765,7 +768,7 @@
765
768
  }
766
769
 
767
770
  /* ---------- Content blocks ---------- */
768
- .fillo-heading { font-size: 1.15em; font-weight: 600; margin: 0.5rem 0 0; }
771
+ .fillo-heading { font-size: 1.125em; font-weight: 600; margin: 0.5rem 0 0; }
769
772
  .fillo-paragraph { color: var(--fillo-muted); line-height: 1.6; margin: 0; }
770
773
  .fillo-divider { border: none; border-top: 1px solid var(--fillo-border); margin: 0.25rem 0; }
771
774
 
@@ -773,7 +776,7 @@
773
776
  .fillo-footer { display: flex; justify-content: flex-end; gap: 0.6rem; }
774
777
  .fillo-button {
775
778
  font: inherit;
776
- font-weight: 550;
779
+ font-weight: 500;
777
780
  border-radius: var(--fillo-radius);
778
781
  padding: 0.6rem 1.4rem;
779
782
  cursor: pointer;
@@ -839,7 +842,7 @@
839
842
  padding: 2rem 1rem;
840
843
  text-align: center;
841
844
  }
842
- .fillo-not-open-title { margin: 0; font-size: 1.05em; font-weight: 650; letter-spacing: 0; }
845
+ .fillo-not-open-title { margin: 0; font-size: 1.125em; font-weight: 600; letter-spacing: 0; }
843
846
  .fillo-not-open-body {
844
847
  max-width: 26rem;
845
848
  margin: 0;
@@ -867,7 +870,7 @@
867
870
  border-bottom: 2.5px solid var(--fillo-primary-contrast);
868
871
  transform: translateY(-0.09rem) rotate(-45deg);
869
872
  }
870
- .fillo-success-title { margin: 0; font-size: 1.05em; letter-spacing: 0; }
873
+ .fillo-success-title { margin: 0; font-weight: 600; font-size: 1.125em; letter-spacing: 0; }
871
874
  .fillo-success-message { margin: 0; font-size: 0.875em; color: var(--fillo-muted); }
872
875
 
873
876
  /* Honeypot — off-screen, never display:none (bots check that) */
@@ -947,12 +950,12 @@
947
950
  border-radius: var(--fillo-radius);
948
951
  background: #fffbeb;
949
952
  color: #92400e;
950
- font-size: 0.82em;
953
+ font-size: 0.875em;
951
954
  line-height: 1.45;
952
955
  }
953
956
  .fillo-devwarning code {
954
957
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
955
- font-size: 0.92em;
958
+ font-size: 1em;
956
959
  background: #fef3c7;
957
960
  padding: 0.05em 0.3em;
958
961
  border-radius: 4px;
@@ -966,7 +969,7 @@
966
969
  border-radius: 999px;
967
970
  background: #fef3c7;
968
971
  color: #92400e;
969
- font-size: 0.7em;
972
+ font-size: 0.75em;
970
973
  font-weight: 600;
971
974
  letter-spacing: 0.06em;
972
975
  text-transform: uppercase;
@@ -984,7 +987,7 @@
984
987
  border-radius: var(--fillo-radius);
985
988
  background: var(--fillo-control-bg);
986
989
  color: var(--fillo-muted);
987
- font-size: 0.82em;
990
+ font-size: 0.875em;
988
991
  line-height: 1.45;
989
992
  }
990
993
  .fillo-resume-clear {
@@ -1024,7 +1027,7 @@
1024
1027
  .fillo-turnstile-error {
1025
1028
  margin: 0;
1026
1029
  color: var(--fillo-error);
1027
- font-size: 0.82em;
1030
+ font-size: 0.875em;
1028
1031
  line-height: 1.45;
1029
1032
  }
1030
1033
 
@@ -1045,7 +1048,7 @@
1045
1048
  justify-content: space-between;
1046
1049
  gap: 0.5rem;
1047
1050
  }
1048
- .fillo-group-instance-title { margin: 0; font-size: 0.95em; font-weight: 600; }
1051
+ .fillo-group-instance-title { margin: 0; font-size: 1.125em; font-weight: 600; }
1049
1052
  .fillo-group-add {
1050
1053
  align-self: flex-start;
1051
1054
  font: inherit;
@@ -1133,7 +1136,7 @@
1133
1136
  .fillo-matrix tbody th { padding: 0.55rem 0 0.15rem; }
1134
1137
  .fillo-matrix td { display: flex; align-items: center; gap: 0.5rem; }
1135
1138
  .fillo-matrix td::before { content: attr(data-label);
1136
- flex: 1 1 auto; font-size: 0.85em; }
1139
+ flex: 1 1 auto; font-size: 0.875em; }
1137
1140
  /* Deviates from the spec's literal ".fillo-matrix .fillo-matrix-cell"
1138
1141
  text (two bare fillo-* classes, no :where()) — that fails this file's
1139
1142
  own specificity lint (styles-lint.test.mjs: "wrap qualifiers in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/react",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Headless React components for embedding Fillo forms natively in your product.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@usefillo/core": "^0.20.0"
46
+ "@usefillo/core": "^0.21.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": "^18.0.0 || ^19.0.0",