@usefillo/react 0.4.0 → 0.5.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 +12 -4
- package/dist/index.js +340 -129
- package/dist/styles.css +64 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -37,8 +37,10 @@ interface FilloFieldIds {
|
|
|
37
37
|
errorId: string;
|
|
38
38
|
name: string;
|
|
39
39
|
}
|
|
40
|
-
interface FieldComponentProps {
|
|
41
|
-
|
|
40
|
+
interface FieldComponentProps<F extends Field = Field> {
|
|
41
|
+
/** Narrow with the type param to skip a cast, e.g.
|
|
42
|
+
* `FieldComponentProps<CustomField>` to read `field.config` directly. */
|
|
43
|
+
field: F;
|
|
42
44
|
value: FieldValue;
|
|
43
45
|
error: string | undefined;
|
|
44
46
|
setValue: (value: FieldValue) => void;
|
|
@@ -174,12 +176,18 @@ interface FieldHandle {
|
|
|
174
176
|
*/
|
|
175
177
|
declare function useField(fieldId: string): FieldHandle;
|
|
176
178
|
|
|
177
|
-
|
|
179
|
+
interface BlockRendererProps {
|
|
178
180
|
block: Block;
|
|
179
181
|
api: FilloApi;
|
|
180
182
|
components?: FieldComponents;
|
|
181
183
|
customComponents?: CustomComponents;
|
|
182
|
-
}
|
|
184
|
+
}
|
|
185
|
+
declare function BlockRendererInner({ block, api, components, customComponents }: BlockRendererProps): react.JSX.Element | null;
|
|
186
|
+
/**
|
|
187
|
+
* Memoized so a keystroke in one field doesn't re-render every other field —
|
|
188
|
+
* see blockPropsEqual for exactly what a block is allowed to depend on.
|
|
189
|
+
*/
|
|
190
|
+
declare const BlockRenderer: react.MemoExoticComponent<typeof BlockRendererInner>;
|
|
183
191
|
/**
|
|
184
192
|
* Render a single field by id using the default (or overridden) component —
|
|
185
193
|
* drop it anywhere inside a <FilloProvider> to place fields in your own
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/FilloForm.tsx
|
|
4
|
-
import { useEffect as useEffect7, useId as useId2, useMemo as
|
|
4
|
+
import { useEffect as useEffect7, useId as useId2, useMemo as useMemo4, useRef as useRef5, useState as useState6 } from "react";
|
|
5
5
|
import {
|
|
6
6
|
FilloError as FilloError2,
|
|
7
7
|
createClient,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
// src/context.ts
|
|
14
14
|
import { allFields } from "@usefillo/core";
|
|
15
|
-
import { createContext, useContext, useId } from "react";
|
|
15
|
+
import { createContext, useContext, useId, useMemo } from "react";
|
|
16
16
|
var FilloContext = createContext(null);
|
|
17
17
|
var FilloInstanceIdContext = createContext(null);
|
|
18
18
|
function useFilloInstanceId() {
|
|
@@ -41,7 +41,12 @@ function useFilloFieldIds(fieldId, ids) {
|
|
|
41
41
|
}
|
|
42
42
|
function useField(fieldId) {
|
|
43
43
|
const api = useFillo();
|
|
44
|
-
const
|
|
44
|
+
const fieldsById = useMemo(() => {
|
|
45
|
+
const map = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const f of allFields(api.form)) map.set(f.id, f);
|
|
47
|
+
return map;
|
|
48
|
+
}, [api.form]);
|
|
49
|
+
const field = fieldsById.get(fieldId);
|
|
45
50
|
return {
|
|
46
51
|
field,
|
|
47
52
|
value: api.data[fieldId],
|
|
@@ -51,7 +56,7 @@ function useField(fieldId) {
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// src/controller.ts
|
|
54
|
-
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
59
|
+
import { useEffect, useMemo as useMemo2, useRef, useSyncExternalStore } from "react";
|
|
55
60
|
import {
|
|
56
61
|
createFormController
|
|
57
62
|
} from "@usefillo/core";
|
|
@@ -78,17 +83,20 @@ function useFilloController(options) {
|
|
|
78
83
|
}, [store, options.form, options.formId, options.client]);
|
|
79
84
|
useEffect(() => () => store.destroy(), [store]);
|
|
80
85
|
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
return useMemo2(
|
|
87
|
+
() => ({
|
|
88
|
+
form: options.form,
|
|
89
|
+
formId: options.formId,
|
|
90
|
+
client: options.client,
|
|
91
|
+
...state,
|
|
92
|
+
setValue: store.setValue,
|
|
93
|
+
setUploading: store.setUploading,
|
|
94
|
+
next: store.next,
|
|
95
|
+
back: store.back,
|
|
96
|
+
submit: store.submit
|
|
97
|
+
}),
|
|
98
|
+
[store, state, options.form, options.formId, options.client]
|
|
99
|
+
);
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
// src/define.ts
|
|
@@ -123,7 +131,7 @@ function useCodeFormSync(form, client, onError) {
|
|
|
123
131
|
|
|
124
132
|
// src/fields.tsx
|
|
125
133
|
import { isField, pipeBlock, validateResponse } from "@usefillo/core";
|
|
126
|
-
import { useEffect as useEffect6, useState as useState5 } from "react";
|
|
134
|
+
import { memo, useEffect as useEffect6, useState as useState5 } from "react";
|
|
127
135
|
|
|
128
136
|
// src/phone.tsx
|
|
129
137
|
import {
|
|
@@ -135,7 +143,7 @@ import {
|
|
|
135
143
|
parsePhone,
|
|
136
144
|
toE164
|
|
137
145
|
} from "@usefillo/core";
|
|
138
|
-
import { useEffect as useEffect3, useLayoutEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
146
|
+
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
|
|
139
147
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
140
148
|
function localeCountry() {
|
|
141
149
|
if (typeof navigator === "undefined") return void 0;
|
|
@@ -234,7 +242,7 @@ function CountrySelect({
|
|
|
234
242
|
const rootRef = useRef2(null);
|
|
235
243
|
const searchRef = useRef2(null);
|
|
236
244
|
const listId = `${labelledBy}-countries`;
|
|
237
|
-
const matches =
|
|
245
|
+
const matches = useMemo3(() => {
|
|
238
246
|
const q = query.trim().toLowerCase();
|
|
239
247
|
if (!q) return PHONE_COUNTRIES;
|
|
240
248
|
const qDigits = q.replace(/[^\d]/g, "");
|
|
@@ -357,9 +365,11 @@ import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } fro
|
|
|
357
365
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
358
366
|
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
359
367
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
368
|
+
const typedInputId = `${ids.inputId}-typed`;
|
|
360
369
|
const canvasRef = useRef3(null);
|
|
361
370
|
const drawing = useRef3(false);
|
|
362
371
|
const [hasInk, setHasInk] = useState3(Boolean(value));
|
|
372
|
+
const [typedName, setTypedName] = useState3("");
|
|
363
373
|
useEffect4(() => {
|
|
364
374
|
const canvas = canvasRef.current;
|
|
365
375
|
if (!canvas) return;
|
|
@@ -387,12 +397,31 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
387
397
|
function start(e) {
|
|
388
398
|
drawing.current = true;
|
|
389
399
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
400
|
+
if (typedName) setTypedName("");
|
|
390
401
|
const ctx = e.currentTarget.getContext("2d");
|
|
391
402
|
if (!ctx) return;
|
|
392
403
|
const { x, y } = point(e);
|
|
393
404
|
ctx.beginPath();
|
|
394
405
|
ctx.moveTo(x, y);
|
|
395
406
|
}
|
|
407
|
+
function drawTypedName(text) {
|
|
408
|
+
const canvas = canvasRef.current;
|
|
409
|
+
const ctx = canvas?.getContext("2d");
|
|
410
|
+
if (!canvas || !ctx) return;
|
|
411
|
+
const rect = canvas.getBoundingClientRect();
|
|
412
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
413
|
+
if (text.trim()) {
|
|
414
|
+
ctx.fillStyle = getComputedStyle(canvas).color;
|
|
415
|
+
ctx.font = `italic ${Math.min(40, rect.height * 0.45)}px "Segoe Script", "Brush Script MT", cursive`;
|
|
416
|
+
ctx.textBaseline = "middle";
|
|
417
|
+
ctx.fillText(text, 12, rect.height / 2);
|
|
418
|
+
setHasInk(true);
|
|
419
|
+
setValue(canvas.toDataURL("image/png"));
|
|
420
|
+
} else {
|
|
421
|
+
setHasInk(false);
|
|
422
|
+
setValue(null);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
396
425
|
function move(e) {
|
|
397
426
|
if (!drawing.current) return;
|
|
398
427
|
const ctx = e.currentTarget.getContext("2d");
|
|
@@ -413,33 +442,63 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
413
442
|
if (!canvas || !ctx) return;
|
|
414
443
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
415
444
|
setHasInk(false);
|
|
445
|
+
setTypedName("");
|
|
416
446
|
setValue(null);
|
|
417
447
|
}
|
|
418
448
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
419
449
|
return /* @__PURE__ */ jsxs2("div", { className: `fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`, children: [
|
|
420
|
-
/* @__PURE__ */ jsxs2("
|
|
450
|
+
/* @__PURE__ */ jsxs2("span", { className: "fillo-label", id: ids.labelId, children: [
|
|
421
451
|
field.label,
|
|
422
452
|
!field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: " (optional)" })
|
|
423
453
|
] }),
|
|
424
454
|
field.description && /* @__PURE__ */ jsx2("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
425
|
-
/* @__PURE__ */ jsxs2(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
455
|
+
/* @__PURE__ */ jsxs2(
|
|
456
|
+
"div",
|
|
457
|
+
{
|
|
458
|
+
role: "group",
|
|
459
|
+
"aria-labelledby": ids.labelId,
|
|
460
|
+
"aria-describedby": describedBy,
|
|
461
|
+
children: [
|
|
462
|
+
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature", children: [
|
|
463
|
+
/* @__PURE__ */ jsx2(
|
|
464
|
+
"canvas",
|
|
465
|
+
{
|
|
466
|
+
ref: canvasRef,
|
|
467
|
+
className: "fillo-signature-canvas",
|
|
468
|
+
"aria-hidden": "true",
|
|
469
|
+
onPointerDown: start,
|
|
470
|
+
onPointerMove: move,
|
|
471
|
+
onPointerUp: end,
|
|
472
|
+
onPointerLeave: end
|
|
473
|
+
}
|
|
474
|
+
),
|
|
475
|
+
hasInk && /* @__PURE__ */ jsx2("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
|
|
476
|
+
!hasInk && /* @__PURE__ */ jsx2("span", { className: "fillo-signature-hint", "aria-hidden": "true", children: "Sign here" })
|
|
477
|
+
] }),
|
|
478
|
+
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature-type", children: [
|
|
479
|
+
/* @__PURE__ */ jsx2("label", { className: "fillo-signature-type-label", htmlFor: typedInputId, children: "Or type your full name to sign" }),
|
|
480
|
+
/* @__PURE__ */ jsx2(
|
|
481
|
+
"input",
|
|
482
|
+
{
|
|
483
|
+
id: typedInputId,
|
|
484
|
+
type: "text",
|
|
485
|
+
className: "fillo-input fillo-signature-type-input",
|
|
486
|
+
autoComplete: "name",
|
|
487
|
+
value: typedName,
|
|
488
|
+
required: field.required || void 0,
|
|
489
|
+
"aria-required": field.required || void 0,
|
|
490
|
+
"aria-invalid": error ? true : void 0,
|
|
491
|
+
"aria-describedby": describedBy,
|
|
492
|
+
onChange: (e) => {
|
|
493
|
+
setTypedName(e.target.value);
|
|
494
|
+
drawTypedName(e.target.value);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
] })
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
),
|
|
443
502
|
error && /* @__PURE__ */ jsx2("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
444
503
|
] });
|
|
445
504
|
}
|
|
@@ -447,6 +506,21 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
447
506
|
// src/upload.tsx
|
|
448
507
|
import { useEffect as useEffect5, useRef as useRef4, useState as useState4 } from "react";
|
|
449
508
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
509
|
+
function uploadStatus(inFlight, doneCount) {
|
|
510
|
+
const failed = inFlight.filter((f) => f.error).length;
|
|
511
|
+
const active = inFlight.length - failed;
|
|
512
|
+
const parts = [];
|
|
513
|
+
if (active > 0) {
|
|
514
|
+
parts.push(`Uploading ${active} ${active === 1 ? "file" : "files"}, please wait\u2026`);
|
|
515
|
+
}
|
|
516
|
+
if (failed > 0) {
|
|
517
|
+
parts.push(`${failed} ${failed === 1 ? "upload" : "uploads"} failed.`);
|
|
518
|
+
}
|
|
519
|
+
if (active === 0 && doneCount > 0) {
|
|
520
|
+
parts.push(`${doneCount} ${doneCount === 1 ? "file" : "files"} uploaded.`);
|
|
521
|
+
}
|
|
522
|
+
return parts.join(" ");
|
|
523
|
+
}
|
|
450
524
|
function formatBytes(bytes) {
|
|
451
525
|
if (bytes < 1024) return `${bytes} B`;
|
|
452
526
|
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
@@ -584,6 +658,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
584
658
|
]
|
|
585
659
|
}
|
|
586
660
|
),
|
|
661
|
+
/* @__PURE__ */ jsx3("div", { role: "status", "aria-live": "polite", className: "fillo-sr-only", children: uploadStatus(inFlight, files.length) }),
|
|
587
662
|
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
588
663
|
files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
|
|
589
664
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
@@ -599,39 +674,53 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
599
674
|
}
|
|
600
675
|
)
|
|
601
676
|
] }, f.fileId)),
|
|
602
|
-
inFlight.map((f) => /* @__PURE__ */ jsxs3(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
"
|
|
608
|
-
{
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
"
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
677
|
+
inFlight.map((f) => /* @__PURE__ */ jsxs3(
|
|
678
|
+
"li",
|
|
679
|
+
{
|
|
680
|
+
className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
|
|
681
|
+
...f.error ? {} : {
|
|
682
|
+
role: "progressbar",
|
|
683
|
+
"aria-label": `Uploading ${f.name}`,
|
|
684
|
+
"aria-valuemin": 0,
|
|
685
|
+
"aria-valuemax": 100,
|
|
686
|
+
"aria-valuenow": Math.round(f.fraction * 100)
|
|
687
|
+
},
|
|
688
|
+
children: [
|
|
689
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
690
|
+
f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
691
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
|
|
692
|
+
/* @__PURE__ */ jsx3(
|
|
693
|
+
"button",
|
|
694
|
+
{
|
|
695
|
+
type: "button",
|
|
696
|
+
className: "fillo-file-remove",
|
|
697
|
+
"aria-label": "Dismiss",
|
|
698
|
+
onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
|
|
699
|
+
children: "\xD7"
|
|
700
|
+
}
|
|
701
|
+
)
|
|
702
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
703
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-file-meta", children: [
|
|
704
|
+
Math.round(f.fraction * 100),
|
|
705
|
+
"% of ",
|
|
706
|
+
formatBytes(f.size)
|
|
707
|
+
] }),
|
|
708
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
|
|
709
|
+
/* @__PURE__ */ jsx3(
|
|
710
|
+
"button",
|
|
711
|
+
{
|
|
712
|
+
type: "button",
|
|
713
|
+
className: "fillo-file-remove",
|
|
714
|
+
"aria-label": `Cancel ${f.name}`,
|
|
715
|
+
onClick: () => cancelUpload(f.key),
|
|
716
|
+
children: "\xD7"
|
|
717
|
+
}
|
|
718
|
+
)
|
|
719
|
+
] })
|
|
720
|
+
]
|
|
721
|
+
},
|
|
722
|
+
f.key
|
|
723
|
+
))
|
|
635
724
|
] }),
|
|
636
725
|
error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
637
726
|
] });
|
|
@@ -782,12 +871,14 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
782
871
|
},
|
|
783
872
|
opt.id
|
|
784
873
|
)),
|
|
785
|
-
choice.allowOther &&
|
|
786
|
-
|
|
874
|
+
choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
|
|
875
|
+
// toggle the radio. The radio + "Other" text keep their own <label>.
|
|
876
|
+
/* @__PURE__ */ jsxs4(
|
|
877
|
+
"div",
|
|
787
878
|
{
|
|
788
879
|
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
789
880
|
children: [
|
|
790
|
-
/* @__PURE__ */ jsxs4("
|
|
881
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
791
882
|
/* @__PURE__ */ jsx4(
|
|
792
883
|
"input",
|
|
793
884
|
{
|
|
@@ -808,6 +899,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
808
899
|
{
|
|
809
900
|
type: "text",
|
|
810
901
|
className: "fillo-input fillo-other-input",
|
|
902
|
+
"aria-label": "Other \u2014 please specify",
|
|
811
903
|
placeholder: "Your answer",
|
|
812
904
|
autoFocus: !isOtherValue,
|
|
813
905
|
value: isOtherValue ? String(value) : "",
|
|
@@ -860,12 +952,14 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
860
952
|
},
|
|
861
953
|
opt.id
|
|
862
954
|
)),
|
|
863
|
-
choice.allowOther &&
|
|
864
|
-
|
|
955
|
+
choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
|
|
956
|
+
// toggle the checkbox. The checkbox + "Other" text keep their own <label>.
|
|
957
|
+
/* @__PURE__ */ jsxs4(
|
|
958
|
+
"div",
|
|
865
959
|
{
|
|
866
960
|
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
867
961
|
children: [
|
|
868
|
-
/* @__PURE__ */ jsxs4("
|
|
962
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
869
963
|
/* @__PURE__ */ jsx4(
|
|
870
964
|
"input",
|
|
871
965
|
{
|
|
@@ -889,6 +983,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
889
983
|
{
|
|
890
984
|
type: "text",
|
|
891
985
|
className: "fillo-input fillo-other-input",
|
|
986
|
+
"aria-label": "Other \u2014 please specify",
|
|
892
987
|
placeholder: "Your answer",
|
|
893
988
|
autoFocus: otherText === void 0,
|
|
894
989
|
value: otherText ?? "",
|
|
@@ -945,6 +1040,7 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
945
1040
|
{
|
|
946
1041
|
type: "text",
|
|
947
1042
|
className: "fillo-input fillo-other-input fillo-other-input--block",
|
|
1043
|
+
"aria-label": "Other \u2014 please specify",
|
|
948
1044
|
placeholder: "Your answer",
|
|
949
1045
|
autoFocus: !isOtherValue,
|
|
950
1046
|
value: isOtherValue ? String(value) : "",
|
|
@@ -1018,22 +1114,62 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1018
1114
|
}
|
|
1019
1115
|
);
|
|
1020
1116
|
}
|
|
1117
|
+
function radiogroupKeyDown(e, steps, current, setValue) {
|
|
1118
|
+
const forward = e.key === "ArrowRight" || e.key === "ArrowDown";
|
|
1119
|
+
const backward = e.key === "ArrowLeft" || e.key === "ArrowUp";
|
|
1120
|
+
if (!forward && !backward) return;
|
|
1121
|
+
e.preventDefault();
|
|
1122
|
+
const index = current == null ? -1 : steps.indexOf(current);
|
|
1123
|
+
let next;
|
|
1124
|
+
if (index === -1) {
|
|
1125
|
+
next = forward ? 0 : steps.length - 1;
|
|
1126
|
+
} else {
|
|
1127
|
+
next = forward ? index + 1 : index - 1;
|
|
1128
|
+
next = (next + steps.length) % steps.length;
|
|
1129
|
+
}
|
|
1130
|
+
const value = steps[next];
|
|
1131
|
+
if (value === void 0) return;
|
|
1132
|
+
setValue(value);
|
|
1133
|
+
const group = e.currentTarget;
|
|
1134
|
+
const buttons = group.querySelectorAll('[role="radio"]');
|
|
1135
|
+
buttons[next]?.focus();
|
|
1136
|
+
}
|
|
1137
|
+
function rovingIndex(steps, current) {
|
|
1138
|
+
if (current == null) return 0;
|
|
1139
|
+
const i = steps.indexOf(current);
|
|
1140
|
+
return i === -1 ? 0 : i;
|
|
1141
|
+
}
|
|
1021
1142
|
function Rating({ field, value, error, setValue, ids: providedIds }) {
|
|
1022
1143
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1023
1144
|
const max = field.max ?? 5;
|
|
1024
1145
|
const current = typeof value === "number" ? value : 0;
|
|
1025
|
-
|
|
1026
|
-
|
|
1146
|
+
const steps = Array.from({ length: max }, (_, i) => i + 1);
|
|
1147
|
+
const selected = current > 0 ? current : null;
|
|
1148
|
+
const tabbable = rovingIndex(steps, selected);
|
|
1149
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1150
|
+
"div",
|
|
1027
1151
|
{
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
"aria-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
children:
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1152
|
+
className: "fillo-rating",
|
|
1153
|
+
role: "radiogroup",
|
|
1154
|
+
"aria-labelledby": ids.labelId,
|
|
1155
|
+
...fieldAria(field, error, ids),
|
|
1156
|
+
onKeyDown: (e) => radiogroupKeyDown(e, steps, selected, (n) => setValue(n)),
|
|
1157
|
+
children: steps.map((n, i) => /* @__PURE__ */ jsx4(
|
|
1158
|
+
"button",
|
|
1159
|
+
{
|
|
1160
|
+
type: "button",
|
|
1161
|
+
className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
|
|
1162
|
+
role: "radio",
|
|
1163
|
+
"aria-label": `${n} of ${max}`,
|
|
1164
|
+
"aria-checked": n === current,
|
|
1165
|
+
tabIndex: i === tabbable ? 0 : -1,
|
|
1166
|
+
onClick: () => setValue(n === current ? null : n),
|
|
1167
|
+
children: "\u2605"
|
|
1168
|
+
},
|
|
1169
|
+
n
|
|
1170
|
+
))
|
|
1171
|
+
}
|
|
1172
|
+
) });
|
|
1037
1173
|
}
|
|
1038
1174
|
function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
1039
1175
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
@@ -1041,21 +1177,47 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1041
1177
|
const min = scale.min ?? 1;
|
|
1042
1178
|
const max = scale.max ?? 10;
|
|
1043
1179
|
const steps = Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
1180
|
+
const current = typeof value === "number" ? value : null;
|
|
1181
|
+
const tabbable = rovingIndex(steps, current);
|
|
1182
|
+
const hasLabels = Boolean(scale.minLabel || scale.maxLabel);
|
|
1183
|
+
const minLabelId = `${ids.inputId}-min`;
|
|
1184
|
+
const maxLabelId = `${ids.inputId}-max`;
|
|
1185
|
+
const describedBy = [
|
|
1186
|
+
scale.minLabel ? minLabelId : null,
|
|
1187
|
+
scale.maxLabel ? maxLabelId : null,
|
|
1188
|
+
field.description ? ids.descriptionId : null,
|
|
1189
|
+
error ? ids.errorId : null
|
|
1190
|
+
].filter(Boolean).join(" ") || void 0;
|
|
1044
1191
|
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
1045
|
-
/* @__PURE__ */ jsx4(
|
|
1046
|
-
"
|
|
1192
|
+
/* @__PURE__ */ jsx4(
|
|
1193
|
+
"div",
|
|
1047
1194
|
{
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
"aria-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1195
|
+
className: "fillo-scale",
|
|
1196
|
+
role: "radiogroup",
|
|
1197
|
+
"aria-labelledby": ids.labelId,
|
|
1198
|
+
"aria-invalid": error ? true : void 0,
|
|
1199
|
+
"aria-required": field.required ? true : void 0,
|
|
1200
|
+
"aria-describedby": describedBy,
|
|
1201
|
+
onKeyDown: (e) => radiogroupKeyDown(e, steps, current, (n) => setValue(n)),
|
|
1202
|
+
children: steps.map((n, i) => /* @__PURE__ */ jsx4(
|
|
1203
|
+
"button",
|
|
1204
|
+
{
|
|
1205
|
+
type: "button",
|
|
1206
|
+
className: `fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`,
|
|
1207
|
+
role: "radio",
|
|
1208
|
+
"aria-label": String(n),
|
|
1209
|
+
"aria-checked": value === n,
|
|
1210
|
+
tabIndex: i === tabbable ? 0 : -1,
|
|
1211
|
+
onClick: () => setValue(value === n ? null : n),
|
|
1212
|
+
children: n
|
|
1213
|
+
},
|
|
1214
|
+
n
|
|
1215
|
+
))
|
|
1216
|
+
}
|
|
1217
|
+
),
|
|
1218
|
+
hasLabels && /* @__PURE__ */ jsxs4("div", { className: "fillo-scale-labels", children: [
|
|
1219
|
+
/* @__PURE__ */ jsx4("span", { id: minLabelId, children: scale.minLabel }),
|
|
1220
|
+
/* @__PURE__ */ jsx4("span", { id: maxLabelId, children: scale.maxLabel })
|
|
1059
1221
|
] })
|
|
1060
1222
|
] });
|
|
1061
1223
|
}
|
|
@@ -1117,20 +1279,30 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1117
1279
|
/* @__PURE__ */ jsx4("th", {}),
|
|
1118
1280
|
matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
|
|
1119
1281
|
] }) }),
|
|
1120
|
-
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) =>
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
"
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1282
|
+
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
|
|
1283
|
+
const rowLabelId = `${ids.inputId}-${row.id}`;
|
|
1284
|
+
const answered = answers[row.id] !== void 0;
|
|
1285
|
+
const rowInvalid = Boolean(error) && field.required && !answered;
|
|
1286
|
+
return (
|
|
1287
|
+
// Each row is its own radiogroup, labelled by the row question.
|
|
1288
|
+
/* @__PURE__ */ jsxs4("tr", { role: "radiogroup", "aria-labelledby": rowLabelId, children: [
|
|
1289
|
+
/* @__PURE__ */ jsx4("th", { scope: "row", id: rowLabelId, children: row.label }),
|
|
1290
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
|
|
1291
|
+
"input",
|
|
1292
|
+
{
|
|
1293
|
+
type: "radio",
|
|
1294
|
+
className: "fillo-option-input",
|
|
1295
|
+
name: `${ids.name}-${row.id}`,
|
|
1296
|
+
"aria-label": `${row.label}: ${col.label}`,
|
|
1297
|
+
"aria-required": field.required ? true : void 0,
|
|
1298
|
+
"aria-invalid": rowInvalid ? true : void 0,
|
|
1299
|
+
checked: answers[row.id] === col.id,
|
|
1300
|
+
onChange: () => setValue({ ...answers, [row.id]: col.id })
|
|
1301
|
+
}
|
|
1302
|
+
) }, col.id))
|
|
1303
|
+
] }, row.id)
|
|
1304
|
+
);
|
|
1305
|
+
}) })
|
|
1134
1306
|
] }) }) });
|
|
1135
1307
|
}
|
|
1136
1308
|
function Hidden() {
|
|
@@ -1183,14 +1355,39 @@ function shouldAutoSubmit(field, value, api) {
|
|
|
1183
1355
|
return false;
|
|
1184
1356
|
}
|
|
1185
1357
|
})();
|
|
1186
|
-
|
|
1358
|
+
if (!hasDiscreteAnswer) return false;
|
|
1359
|
+
if (value === api.data[field.id]) return false;
|
|
1360
|
+
return validateResponse(api.form, { ...api.data, [field.id]: value }).ok;
|
|
1187
1361
|
}
|
|
1188
|
-
function
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1362
|
+
function blockHasPiping(block) {
|
|
1363
|
+
const has = (s) => typeof s === "string" && s.includes("{{");
|
|
1364
|
+
switch (block.kind) {
|
|
1365
|
+
case "heading":
|
|
1366
|
+
case "paragraph":
|
|
1367
|
+
return has(block.text);
|
|
1368
|
+
case "divider":
|
|
1369
|
+
return false;
|
|
1370
|
+
default:
|
|
1371
|
+
return has(block.label) || has(block.description);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function blockPropsEqual(prev, next) {
|
|
1375
|
+
if (prev.block !== next.block || prev.components !== next.components || prev.customComponents !== next.customComponents) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
const id = next.block.id;
|
|
1379
|
+
if (prev.api.data[id] !== next.api.data[id]) return false;
|
|
1380
|
+
if (prev.api.errors[id] !== next.api.errors[id]) return false;
|
|
1381
|
+
const needsWiderState = next.block.kind === "custom" || blockHasPiping(prev.block) || blockHasPiping(next.block);
|
|
1382
|
+
if (needsWiderState && prev.api.data !== next.api.data) return false;
|
|
1383
|
+
if (next.api.form.settings.submitMode === "auto") {
|
|
1384
|
+
if (prev.api.data !== next.api.data || prev.api.status !== next.api.status || prev.api.uploading !== next.api.uploading || prev.api.isLastPage !== next.api.isLastPage) {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
function BlockRendererInner({ block, api, components, customComponents }) {
|
|
1194
1391
|
block = pipeBlock(block, api.data, api.form);
|
|
1195
1392
|
const ids = useFilloFieldIds(block.id);
|
|
1196
1393
|
if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
|
|
@@ -1224,6 +1421,7 @@ function BlockRenderer({
|
|
|
1224
1421
|
}
|
|
1225
1422
|
);
|
|
1226
1423
|
}
|
|
1424
|
+
var BlockRenderer = memo(BlockRendererInner, blockPropsEqual);
|
|
1227
1425
|
function FormField({
|
|
1228
1426
|
id,
|
|
1229
1427
|
components,
|
|
@@ -1276,6 +1474,15 @@ function badgeUrl(href) {
|
|
|
1276
1474
|
url.searchParams.set("utm_source", "form_badge");
|
|
1277
1475
|
return url.toString();
|
|
1278
1476
|
}
|
|
1477
|
+
function focusFirstInvalid(formEl) {
|
|
1478
|
+
if (!formEl || typeof window === "undefined") return;
|
|
1479
|
+
const run = () => {
|
|
1480
|
+
const target = formEl.querySelector('[aria-invalid="true"]');
|
|
1481
|
+
target?.focus?.();
|
|
1482
|
+
};
|
|
1483
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
1484
|
+
else setTimeout(run, 0);
|
|
1485
|
+
}
|
|
1279
1486
|
function isAutoSubmitBlock(block) {
|
|
1280
1487
|
return isField2(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
1281
1488
|
}
|
|
@@ -1284,7 +1491,7 @@ function needsExplicitSubmit(blocks) {
|
|
|
1284
1491
|
}
|
|
1285
1492
|
function FilloForm(props) {
|
|
1286
1493
|
const { form, formId } = props;
|
|
1287
|
-
const client =
|
|
1494
|
+
const client = useMemo4(
|
|
1288
1495
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
1289
1496
|
[props.client, formId]
|
|
1290
1497
|
);
|
|
@@ -1320,7 +1527,7 @@ function FilloForm(props) {
|
|
|
1320
1527
|
};
|
|
1321
1528
|
}, [needsFetch, client, formId, onError]);
|
|
1322
1529
|
const rawSchema = inlineSchema ?? fetched;
|
|
1323
|
-
const normalized =
|
|
1530
|
+
const normalized = useMemo4(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1324
1531
|
const schema = normalized?.ok ? normalized.schema : null;
|
|
1325
1532
|
const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
|
|
1326
1533
|
if (normalized && !normalized.ok) {
|
|
@@ -1396,7 +1603,7 @@ function ResolvedForm(props) {
|
|
|
1396
1603
|
skipValidation: props.skipValidation,
|
|
1397
1604
|
getHoneypot: () => hpRef.current
|
|
1398
1605
|
});
|
|
1399
|
-
const themed =
|
|
1606
|
+
const themed = useMemo4(() => themeProps(theme), [theme]);
|
|
1400
1607
|
const settings = schema.settings;
|
|
1401
1608
|
const submitted = api.status === "submitted";
|
|
1402
1609
|
useEffect7(() => {
|
|
@@ -1426,8 +1633,12 @@ function ResolvedForm(props) {
|
|
|
1426
1633
|
noValidate: true,
|
|
1427
1634
|
onSubmit: (e) => {
|
|
1428
1635
|
e.preventDefault();
|
|
1429
|
-
|
|
1430
|
-
|
|
1636
|
+
const formEl = e.currentTarget;
|
|
1637
|
+
if (api.isLastPage) void api.submit().then(() => focusFirstInvalid(formEl));
|
|
1638
|
+
else {
|
|
1639
|
+
api.next();
|
|
1640
|
+
focusFirstInvalid(formEl);
|
|
1641
|
+
}
|
|
1431
1642
|
},
|
|
1432
1643
|
children: [
|
|
1433
1644
|
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
@@ -1498,15 +1709,15 @@ function ResolvedForm(props) {
|
|
|
1498
1709
|
}
|
|
1499
1710
|
|
|
1500
1711
|
// src/provider.tsx
|
|
1501
|
-
import { useId as useId3, useMemo as
|
|
1712
|
+
import { useId as useId3, useMemo as useMemo5 } from "react";
|
|
1502
1713
|
import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2 } from "@usefillo/core";
|
|
1503
1714
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1504
1715
|
function FilloProvider({ children, form, formId, client, ...options }) {
|
|
1505
1716
|
const instanceId = useId3().replace(/:/g, "");
|
|
1506
1717
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1507
1718
|
const rawSchema = codeForm ? codeForm.schema : form;
|
|
1508
|
-
const schemaKey = JSON.stringify(rawSchema);
|
|
1509
|
-
const normalized =
|
|
1719
|
+
const schemaKey = useMemo5(() => JSON.stringify(rawSchema), [rawSchema]);
|
|
1720
|
+
const normalized = useMemo5(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
|
|
1510
1721
|
if (!normalized.ok) {
|
|
1511
1722
|
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1512
1723
|
}
|
package/dist/styles.css
CHANGED
|
@@ -328,6 +328,11 @@
|
|
|
328
328
|
}
|
|
329
329
|
.fillo-star:hover { transform: scale(1.15); }
|
|
330
330
|
.fillo-star--active { color: var(--fillo-primary); }
|
|
331
|
+
.fillo-star:focus-visible {
|
|
332
|
+
outline: none;
|
|
333
|
+
border-radius: calc(var(--fillo-radius) * 0.5);
|
|
334
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 30%, transparent);
|
|
335
|
+
}
|
|
331
336
|
|
|
332
337
|
/* ---------- Linear scale ---------- */
|
|
333
338
|
/* Steps share one row and shrink to fit — an 11-step NPS scale must never
|
|
@@ -348,6 +353,11 @@
|
|
|
348
353
|
transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
|
|
349
354
|
}
|
|
350
355
|
.fillo-scale-step:hover { border-color: var(--fillo-primary); }
|
|
356
|
+
.fillo-scale-step:focus-visible {
|
|
357
|
+
outline: none;
|
|
358
|
+
border-color: var(--fillo-primary);
|
|
359
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
|
|
360
|
+
}
|
|
351
361
|
.fillo-scale-step--active {
|
|
352
362
|
background: var(--fillo-primary);
|
|
353
363
|
border-color: var(--fillo-primary);
|
|
@@ -450,6 +460,21 @@
|
|
|
450
460
|
}
|
|
451
461
|
.fillo-signature-clear:hover { color: var(--fillo-error); border-color: var(--fillo-error); }
|
|
452
462
|
|
|
463
|
+
/* Type-to-sign: the keyboard/screen-reader path under the canvas. */
|
|
464
|
+
.fillo-signature-type {
|
|
465
|
+
display: flex;
|
|
466
|
+
flex-direction: column;
|
|
467
|
+
gap: 0.35rem;
|
|
468
|
+
margin-top: 0.55rem;
|
|
469
|
+
}
|
|
470
|
+
.fillo-signature-type-label {
|
|
471
|
+
color: var(--fillo-muted);
|
|
472
|
+
font-size: 0.8rem;
|
|
473
|
+
}
|
|
474
|
+
.fillo-signature-type-input {
|
|
475
|
+
margin-top: 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
453
478
|
/* ---------- File upload ---------- */
|
|
454
479
|
.fillo-dropzone {
|
|
455
480
|
display: flex;
|
|
@@ -469,6 +494,11 @@
|
|
|
469
494
|
border-color: var(--fillo-primary);
|
|
470
495
|
background: color-mix(in srgb, var(--fillo-primary) 4%, var(--fillo-control-bg));
|
|
471
496
|
}
|
|
497
|
+
.fillo-dropzone:focus-visible {
|
|
498
|
+
outline: none;
|
|
499
|
+
border-color: var(--fillo-primary);
|
|
500
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
|
|
501
|
+
}
|
|
472
502
|
.fillo-dropzone--disabled { cursor: not-allowed; opacity: 0.6; }
|
|
473
503
|
.fillo-dropzone-title { font-weight: 550; font-size: 0.95rem; }
|
|
474
504
|
.fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82rem; }
|
|
@@ -526,6 +556,10 @@
|
|
|
526
556
|
}
|
|
527
557
|
.fillo-button:active { transform: translateY(1px); }
|
|
528
558
|
.fillo-button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|
559
|
+
.fillo-button:focus-visible {
|
|
560
|
+
outline: none;
|
|
561
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 30%, transparent);
|
|
562
|
+
}
|
|
529
563
|
.fillo-button--primary { background: var(--fillo-primary); color: var(--fillo-primary-contrast); }
|
|
530
564
|
.fillo-button--ghost { background: transparent; color: var(--fillo-text); border-color: var(--fillo-border); }
|
|
531
565
|
|
|
@@ -574,6 +608,20 @@
|
|
|
574
608
|
pointer-events: none;
|
|
575
609
|
}
|
|
576
610
|
|
|
611
|
+
/* Visually hidden but exposed to assistive tech — for aria-live narration. */
|
|
612
|
+
.fillo-sr-only {
|
|
613
|
+
position: absolute;
|
|
614
|
+
width: 1px;
|
|
615
|
+
height: 1px;
|
|
616
|
+
margin: -1px;
|
|
617
|
+
padding: 0;
|
|
618
|
+
overflow: hidden;
|
|
619
|
+
clip: rect(0 0 0 0);
|
|
620
|
+
clip-path: inset(50%);
|
|
621
|
+
white-space: nowrap;
|
|
622
|
+
border: 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
577
625
|
/* ---------- Loading skeleton ---------- */
|
|
578
626
|
.fillo-form--loading { gap: 0.9rem; }
|
|
579
627
|
.fillo-skeleton {
|
|
@@ -588,6 +636,22 @@
|
|
|
588
636
|
to { background-position: -200% 0; }
|
|
589
637
|
}
|
|
590
638
|
|
|
639
|
+
/* Respect a reduced-motion preference: drop transitions, the press nudge,
|
|
640
|
+
the rating hover scale, and the skeleton shimmer. */
|
|
641
|
+
@media (prefers-reduced-motion: reduce) {
|
|
642
|
+
.fillo-form *,
|
|
643
|
+
.fillo-form *::before,
|
|
644
|
+
.fillo-form *::after {
|
|
645
|
+
animation-duration: 0.001ms !important;
|
|
646
|
+
animation-iteration-count: 1 !important;
|
|
647
|
+
transition-duration: 0.001ms !important;
|
|
648
|
+
scroll-behavior: auto !important;
|
|
649
|
+
}
|
|
650
|
+
.fillo-button:active { transform: none; }
|
|
651
|
+
.fillo-star:hover { transform: none; }
|
|
652
|
+
.fillo-skeleton { animation: none; }
|
|
653
|
+
}
|
|
654
|
+
|
|
591
655
|
/* ---------- "Powered by Fillo" badge ---------- */
|
|
592
656
|
.fillo-powered {
|
|
593
657
|
display: block;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefillo/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Headless React components for embedding Fillo forms natively in your product.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@usefillo/core": "^0.
|
|
35
|
+
"@usefillo/core": "^0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"react": "^18.0.0 || ^19.0.0",
|