@usefillo/react 0.4.0 → 0.5.1
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 +351 -134
- 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,18 +1,19 @@
|
|
|
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,
|
|
8
8
|
isField as isField2,
|
|
9
9
|
normalizeFormSchema,
|
|
10
|
-
normalizeFormTheme
|
|
10
|
+
normalizeFormTheme,
|
|
11
|
+
visibleFields as visibleFields2
|
|
11
12
|
} from "@usefillo/core";
|
|
12
13
|
|
|
13
14
|
// src/context.ts
|
|
14
15
|
import { allFields } from "@usefillo/core";
|
|
15
|
-
import { createContext, useContext, useId } from "react";
|
|
16
|
+
import { createContext, useContext, useId, useMemo } from "react";
|
|
16
17
|
var FilloContext = createContext(null);
|
|
17
18
|
var FilloInstanceIdContext = createContext(null);
|
|
18
19
|
function useFilloInstanceId() {
|
|
@@ -41,7 +42,12 @@ function useFilloFieldIds(fieldId, ids) {
|
|
|
41
42
|
}
|
|
42
43
|
function useField(fieldId) {
|
|
43
44
|
const api = useFillo();
|
|
44
|
-
const
|
|
45
|
+
const fieldsById = useMemo(() => {
|
|
46
|
+
const map = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const f of allFields(api.form)) map.set(f.id, f);
|
|
48
|
+
return map;
|
|
49
|
+
}, [api.form]);
|
|
50
|
+
const field = fieldsById.get(fieldId);
|
|
45
51
|
return {
|
|
46
52
|
field,
|
|
47
53
|
value: api.data[fieldId],
|
|
@@ -51,7 +57,7 @@ function useField(fieldId) {
|
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
// src/controller.ts
|
|
54
|
-
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
60
|
+
import { useEffect, useMemo as useMemo2, useRef, useSyncExternalStore } from "react";
|
|
55
61
|
import {
|
|
56
62
|
createFormController
|
|
57
63
|
} from "@usefillo/core";
|
|
@@ -78,17 +84,20 @@ function useFilloController(options) {
|
|
|
78
84
|
}, [store, options.form, options.formId, options.client]);
|
|
79
85
|
useEffect(() => () => store.destroy(), [store]);
|
|
80
86
|
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
return useMemo2(
|
|
88
|
+
() => ({
|
|
89
|
+
form: options.form,
|
|
90
|
+
formId: options.formId,
|
|
91
|
+
client: options.client,
|
|
92
|
+
...state,
|
|
93
|
+
setValue: store.setValue,
|
|
94
|
+
setUploading: store.setUploading,
|
|
95
|
+
next: store.next,
|
|
96
|
+
back: store.back,
|
|
97
|
+
submit: store.submit
|
|
98
|
+
}),
|
|
99
|
+
[store, state, options.form, options.formId, options.client]
|
|
100
|
+
);
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
// src/define.ts
|
|
@@ -122,8 +131,8 @@ function useCodeFormSync(form, client, onError) {
|
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
// src/fields.tsx
|
|
125
|
-
import { isField, pipeBlock, validateResponse } from "@usefillo/core";
|
|
126
|
-
import { useEffect as useEffect6, useState as useState5 } from "react";
|
|
134
|
+
import { isField, pipeBlock, validateResponse, visibleFields } from "@usefillo/core";
|
|
135
|
+
import { memo, useEffect as useEffect6, useState as useState5 } from "react";
|
|
127
136
|
|
|
128
137
|
// src/phone.tsx
|
|
129
138
|
import {
|
|
@@ -135,7 +144,7 @@ import {
|
|
|
135
144
|
parsePhone,
|
|
136
145
|
toE164
|
|
137
146
|
} from "@usefillo/core";
|
|
138
|
-
import { useEffect as useEffect3, useLayoutEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
147
|
+
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
|
|
139
148
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
140
149
|
function localeCountry() {
|
|
141
150
|
if (typeof navigator === "undefined") return void 0;
|
|
@@ -234,7 +243,7 @@ function CountrySelect({
|
|
|
234
243
|
const rootRef = useRef2(null);
|
|
235
244
|
const searchRef = useRef2(null);
|
|
236
245
|
const listId = `${labelledBy}-countries`;
|
|
237
|
-
const matches =
|
|
246
|
+
const matches = useMemo3(() => {
|
|
238
247
|
const q = query.trim().toLowerCase();
|
|
239
248
|
if (!q) return PHONE_COUNTRIES;
|
|
240
249
|
const qDigits = q.replace(/[^\d]/g, "");
|
|
@@ -357,9 +366,11 @@ import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } fro
|
|
|
357
366
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
358
367
|
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
359
368
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
369
|
+
const typedInputId = `${ids.inputId}-typed`;
|
|
360
370
|
const canvasRef = useRef3(null);
|
|
361
371
|
const drawing = useRef3(false);
|
|
362
372
|
const [hasInk, setHasInk] = useState3(Boolean(value));
|
|
373
|
+
const [typedName, setTypedName] = useState3("");
|
|
363
374
|
useEffect4(() => {
|
|
364
375
|
const canvas = canvasRef.current;
|
|
365
376
|
if (!canvas) return;
|
|
@@ -387,12 +398,31 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
387
398
|
function start(e) {
|
|
388
399
|
drawing.current = true;
|
|
389
400
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
401
|
+
if (typedName) setTypedName("");
|
|
390
402
|
const ctx = e.currentTarget.getContext("2d");
|
|
391
403
|
if (!ctx) return;
|
|
392
404
|
const { x, y } = point(e);
|
|
393
405
|
ctx.beginPath();
|
|
394
406
|
ctx.moveTo(x, y);
|
|
395
407
|
}
|
|
408
|
+
function drawTypedName(text) {
|
|
409
|
+
const canvas = canvasRef.current;
|
|
410
|
+
const ctx = canvas?.getContext("2d");
|
|
411
|
+
if (!canvas || !ctx) return;
|
|
412
|
+
const rect = canvas.getBoundingClientRect();
|
|
413
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
414
|
+
if (text.trim()) {
|
|
415
|
+
ctx.fillStyle = getComputedStyle(canvas).color;
|
|
416
|
+
ctx.font = `italic ${Math.min(40, rect.height * 0.45)}px "Segoe Script", "Brush Script MT", cursive`;
|
|
417
|
+
ctx.textBaseline = "middle";
|
|
418
|
+
ctx.fillText(text, 12, rect.height / 2);
|
|
419
|
+
setHasInk(true);
|
|
420
|
+
setValue(canvas.toDataURL("image/png"));
|
|
421
|
+
} else {
|
|
422
|
+
setHasInk(false);
|
|
423
|
+
setValue(null);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
396
426
|
function move(e) {
|
|
397
427
|
if (!drawing.current) return;
|
|
398
428
|
const ctx = e.currentTarget.getContext("2d");
|
|
@@ -413,33 +443,63 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
413
443
|
if (!canvas || !ctx) return;
|
|
414
444
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
415
445
|
setHasInk(false);
|
|
446
|
+
setTypedName("");
|
|
416
447
|
setValue(null);
|
|
417
448
|
}
|
|
418
449
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
419
450
|
return /* @__PURE__ */ jsxs2("div", { className: `fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`, children: [
|
|
420
|
-
/* @__PURE__ */ jsxs2("
|
|
451
|
+
/* @__PURE__ */ jsxs2("span", { className: "fillo-label", id: ids.labelId, children: [
|
|
421
452
|
field.label,
|
|
422
453
|
!field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: " (optional)" })
|
|
423
454
|
] }),
|
|
424
455
|
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
|
-
|
|
456
|
+
/* @__PURE__ */ jsxs2(
|
|
457
|
+
"div",
|
|
458
|
+
{
|
|
459
|
+
role: "group",
|
|
460
|
+
"aria-labelledby": ids.labelId,
|
|
461
|
+
"aria-describedby": describedBy,
|
|
462
|
+
children: [
|
|
463
|
+
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature", children: [
|
|
464
|
+
/* @__PURE__ */ jsx2(
|
|
465
|
+
"canvas",
|
|
466
|
+
{
|
|
467
|
+
ref: canvasRef,
|
|
468
|
+
className: "fillo-signature-canvas",
|
|
469
|
+
"aria-hidden": "true",
|
|
470
|
+
onPointerDown: start,
|
|
471
|
+
onPointerMove: move,
|
|
472
|
+
onPointerUp: end,
|
|
473
|
+
onPointerLeave: end
|
|
474
|
+
}
|
|
475
|
+
),
|
|
476
|
+
hasInk && /* @__PURE__ */ jsx2("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
|
|
477
|
+
!hasInk && /* @__PURE__ */ jsx2("span", { className: "fillo-signature-hint", "aria-hidden": "true", children: "Sign here" })
|
|
478
|
+
] }),
|
|
479
|
+
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature-type", children: [
|
|
480
|
+
/* @__PURE__ */ jsx2("label", { className: "fillo-signature-type-label", htmlFor: typedInputId, children: "Or type your full name to sign" }),
|
|
481
|
+
/* @__PURE__ */ jsx2(
|
|
482
|
+
"input",
|
|
483
|
+
{
|
|
484
|
+
id: typedInputId,
|
|
485
|
+
type: "text",
|
|
486
|
+
className: "fillo-input fillo-signature-type-input",
|
|
487
|
+
autoComplete: "name",
|
|
488
|
+
value: typedName,
|
|
489
|
+
required: field.required || void 0,
|
|
490
|
+
"aria-required": field.required || void 0,
|
|
491
|
+
"aria-invalid": error ? true : void 0,
|
|
492
|
+
"aria-describedby": describedBy,
|
|
493
|
+
onChange: (e) => {
|
|
494
|
+
setTypedName(e.target.value);
|
|
495
|
+
drawTypedName(e.target.value);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
] })
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
),
|
|
443
503
|
error && /* @__PURE__ */ jsx2("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
444
504
|
] });
|
|
445
505
|
}
|
|
@@ -447,6 +507,21 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
447
507
|
// src/upload.tsx
|
|
448
508
|
import { useEffect as useEffect5, useRef as useRef4, useState as useState4 } from "react";
|
|
449
509
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
510
|
+
function uploadStatus(inFlight, doneCount) {
|
|
511
|
+
const failed = inFlight.filter((f) => f.error).length;
|
|
512
|
+
const active = inFlight.length - failed;
|
|
513
|
+
const parts = [];
|
|
514
|
+
if (active > 0) {
|
|
515
|
+
parts.push(`Uploading ${active} ${active === 1 ? "file" : "files"}, please wait\u2026`);
|
|
516
|
+
}
|
|
517
|
+
if (failed > 0) {
|
|
518
|
+
parts.push(`${failed} ${failed === 1 ? "upload" : "uploads"} failed.`);
|
|
519
|
+
}
|
|
520
|
+
if (active === 0 && doneCount > 0) {
|
|
521
|
+
parts.push(`${doneCount} ${doneCount === 1 ? "file" : "files"} uploaded.`);
|
|
522
|
+
}
|
|
523
|
+
return parts.join(" ");
|
|
524
|
+
}
|
|
450
525
|
function formatBytes(bytes) {
|
|
451
526
|
if (bytes < 1024) return `${bytes} B`;
|
|
452
527
|
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
@@ -584,6 +659,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
584
659
|
]
|
|
585
660
|
}
|
|
586
661
|
),
|
|
662
|
+
/* @__PURE__ */ jsx3("div", { role: "status", "aria-live": "polite", className: "fillo-sr-only", children: uploadStatus(inFlight, files.length) }),
|
|
587
663
|
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
588
664
|
files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
|
|
589
665
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
@@ -599,39 +675,53 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
599
675
|
}
|
|
600
676
|
)
|
|
601
677
|
] }, 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
|
-
|
|
678
|
+
inFlight.map((f) => /* @__PURE__ */ jsxs3(
|
|
679
|
+
"li",
|
|
680
|
+
{
|
|
681
|
+
className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
|
|
682
|
+
...f.error ? {} : {
|
|
683
|
+
role: "progressbar",
|
|
684
|
+
"aria-label": `Uploading ${f.name}`,
|
|
685
|
+
"aria-valuemin": 0,
|
|
686
|
+
"aria-valuemax": 100,
|
|
687
|
+
"aria-valuenow": Math.round(f.fraction * 100)
|
|
688
|
+
},
|
|
689
|
+
children: [
|
|
690
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
691
|
+
f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
692
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
|
|
693
|
+
/* @__PURE__ */ jsx3(
|
|
694
|
+
"button",
|
|
695
|
+
{
|
|
696
|
+
type: "button",
|
|
697
|
+
className: "fillo-file-remove",
|
|
698
|
+
"aria-label": "Dismiss",
|
|
699
|
+
onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
|
|
700
|
+
children: "\xD7"
|
|
701
|
+
}
|
|
702
|
+
)
|
|
703
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
704
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-file-meta", children: [
|
|
705
|
+
Math.round(f.fraction * 100),
|
|
706
|
+
"% of ",
|
|
707
|
+
formatBytes(f.size)
|
|
708
|
+
] }),
|
|
709
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
|
|
710
|
+
/* @__PURE__ */ jsx3(
|
|
711
|
+
"button",
|
|
712
|
+
{
|
|
713
|
+
type: "button",
|
|
714
|
+
className: "fillo-file-remove",
|
|
715
|
+
"aria-label": `Cancel ${f.name}`,
|
|
716
|
+
onClick: () => cancelUpload(f.key),
|
|
717
|
+
children: "\xD7"
|
|
718
|
+
}
|
|
719
|
+
)
|
|
720
|
+
] })
|
|
721
|
+
]
|
|
722
|
+
},
|
|
723
|
+
f.key
|
|
724
|
+
))
|
|
635
725
|
] }),
|
|
636
726
|
error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
637
727
|
] });
|
|
@@ -782,12 +872,14 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
782
872
|
},
|
|
783
873
|
opt.id
|
|
784
874
|
)),
|
|
785
|
-
choice.allowOther &&
|
|
786
|
-
|
|
875
|
+
choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
|
|
876
|
+
// toggle the radio. The radio + "Other" text keep their own <label>.
|
|
877
|
+
/* @__PURE__ */ jsxs4(
|
|
878
|
+
"div",
|
|
787
879
|
{
|
|
788
880
|
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
789
881
|
children: [
|
|
790
|
-
/* @__PURE__ */ jsxs4("
|
|
882
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
791
883
|
/* @__PURE__ */ jsx4(
|
|
792
884
|
"input",
|
|
793
885
|
{
|
|
@@ -808,6 +900,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
808
900
|
{
|
|
809
901
|
type: "text",
|
|
810
902
|
className: "fillo-input fillo-other-input",
|
|
903
|
+
"aria-label": "Other \u2014 please specify",
|
|
811
904
|
placeholder: "Your answer",
|
|
812
905
|
autoFocus: !isOtherValue,
|
|
813
906
|
value: isOtherValue ? String(value) : "",
|
|
@@ -860,12 +953,14 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
860
953
|
},
|
|
861
954
|
opt.id
|
|
862
955
|
)),
|
|
863
|
-
choice.allowOther &&
|
|
864
|
-
|
|
956
|
+
choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
|
|
957
|
+
// toggle the checkbox. The checkbox + "Other" text keep their own <label>.
|
|
958
|
+
/* @__PURE__ */ jsxs4(
|
|
959
|
+
"div",
|
|
865
960
|
{
|
|
866
961
|
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
867
962
|
children: [
|
|
868
|
-
/* @__PURE__ */ jsxs4("
|
|
963
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
869
964
|
/* @__PURE__ */ jsx4(
|
|
870
965
|
"input",
|
|
871
966
|
{
|
|
@@ -889,6 +984,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
889
984
|
{
|
|
890
985
|
type: "text",
|
|
891
986
|
className: "fillo-input fillo-other-input",
|
|
987
|
+
"aria-label": "Other \u2014 please specify",
|
|
892
988
|
placeholder: "Your answer",
|
|
893
989
|
autoFocus: otherText === void 0,
|
|
894
990
|
value: otherText ?? "",
|
|
@@ -945,6 +1041,7 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
945
1041
|
{
|
|
946
1042
|
type: "text",
|
|
947
1043
|
className: "fillo-input fillo-other-input fillo-other-input--block",
|
|
1044
|
+
"aria-label": "Other \u2014 please specify",
|
|
948
1045
|
placeholder: "Your answer",
|
|
949
1046
|
autoFocus: !isOtherValue,
|
|
950
1047
|
value: isOtherValue ? String(value) : "",
|
|
@@ -1018,22 +1115,62 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1018
1115
|
}
|
|
1019
1116
|
);
|
|
1020
1117
|
}
|
|
1118
|
+
function radiogroupKeyDown(e, steps, current, setValue) {
|
|
1119
|
+
const forward = e.key === "ArrowRight" || e.key === "ArrowDown";
|
|
1120
|
+
const backward = e.key === "ArrowLeft" || e.key === "ArrowUp";
|
|
1121
|
+
if (!forward && !backward) return;
|
|
1122
|
+
e.preventDefault();
|
|
1123
|
+
const index = current == null ? -1 : steps.indexOf(current);
|
|
1124
|
+
let next;
|
|
1125
|
+
if (index === -1) {
|
|
1126
|
+
next = forward ? 0 : steps.length - 1;
|
|
1127
|
+
} else {
|
|
1128
|
+
next = forward ? index + 1 : index - 1;
|
|
1129
|
+
next = (next + steps.length) % steps.length;
|
|
1130
|
+
}
|
|
1131
|
+
const value = steps[next];
|
|
1132
|
+
if (value === void 0) return;
|
|
1133
|
+
setValue(value);
|
|
1134
|
+
const group = e.currentTarget;
|
|
1135
|
+
const buttons = group.querySelectorAll('[role="radio"]');
|
|
1136
|
+
buttons[next]?.focus();
|
|
1137
|
+
}
|
|
1138
|
+
function rovingIndex(steps, current) {
|
|
1139
|
+
if (current == null) return 0;
|
|
1140
|
+
const i = steps.indexOf(current);
|
|
1141
|
+
return i === -1 ? 0 : i;
|
|
1142
|
+
}
|
|
1021
1143
|
function Rating({ field, value, error, setValue, ids: providedIds }) {
|
|
1022
1144
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1023
1145
|
const max = field.max ?? 5;
|
|
1024
1146
|
const current = typeof value === "number" ? value : 0;
|
|
1025
|
-
|
|
1026
|
-
|
|
1147
|
+
const steps = Array.from({ length: max }, (_, i) => i + 1);
|
|
1148
|
+
const selected = current > 0 ? current : null;
|
|
1149
|
+
const tabbable = rovingIndex(steps, selected);
|
|
1150
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1151
|
+
"div",
|
|
1027
1152
|
{
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
"aria-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
children:
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1153
|
+
className: "fillo-rating",
|
|
1154
|
+
role: "radiogroup",
|
|
1155
|
+
"aria-labelledby": ids.labelId,
|
|
1156
|
+
...fieldAria(field, error, ids),
|
|
1157
|
+
onKeyDown: (e) => radiogroupKeyDown(e, steps, selected, (n) => setValue(n)),
|
|
1158
|
+
children: steps.map((n, i) => /* @__PURE__ */ jsx4(
|
|
1159
|
+
"button",
|
|
1160
|
+
{
|
|
1161
|
+
type: "button",
|
|
1162
|
+
className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
|
|
1163
|
+
role: "radio",
|
|
1164
|
+
"aria-label": `${n} of ${max}`,
|
|
1165
|
+
"aria-checked": n === current,
|
|
1166
|
+
tabIndex: i === tabbable ? 0 : -1,
|
|
1167
|
+
onClick: () => setValue(n === current ? null : n),
|
|
1168
|
+
children: "\u2605"
|
|
1169
|
+
},
|
|
1170
|
+
n
|
|
1171
|
+
))
|
|
1172
|
+
}
|
|
1173
|
+
) });
|
|
1037
1174
|
}
|
|
1038
1175
|
function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
1039
1176
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
@@ -1041,21 +1178,47 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1041
1178
|
const min = scale.min ?? 1;
|
|
1042
1179
|
const max = scale.max ?? 10;
|
|
1043
1180
|
const steps = Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
1181
|
+
const current = typeof value === "number" ? value : null;
|
|
1182
|
+
const tabbable = rovingIndex(steps, current);
|
|
1183
|
+
const hasLabels = Boolean(scale.minLabel || scale.maxLabel);
|
|
1184
|
+
const minLabelId = `${ids.inputId}-min`;
|
|
1185
|
+
const maxLabelId = `${ids.inputId}-max`;
|
|
1186
|
+
const describedBy = [
|
|
1187
|
+
scale.minLabel ? minLabelId : null,
|
|
1188
|
+
scale.maxLabel ? maxLabelId : null,
|
|
1189
|
+
field.description ? ids.descriptionId : null,
|
|
1190
|
+
error ? ids.errorId : null
|
|
1191
|
+
].filter(Boolean).join(" ") || void 0;
|
|
1044
1192
|
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
1045
|
-
/* @__PURE__ */ jsx4(
|
|
1046
|
-
"
|
|
1193
|
+
/* @__PURE__ */ jsx4(
|
|
1194
|
+
"div",
|
|
1047
1195
|
{
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
"aria-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1196
|
+
className: "fillo-scale",
|
|
1197
|
+
role: "radiogroup",
|
|
1198
|
+
"aria-labelledby": ids.labelId,
|
|
1199
|
+
"aria-invalid": error ? true : void 0,
|
|
1200
|
+
"aria-required": field.required ? true : void 0,
|
|
1201
|
+
"aria-describedby": describedBy,
|
|
1202
|
+
onKeyDown: (e) => radiogroupKeyDown(e, steps, current, (n) => setValue(n)),
|
|
1203
|
+
children: steps.map((n, i) => /* @__PURE__ */ jsx4(
|
|
1204
|
+
"button",
|
|
1205
|
+
{
|
|
1206
|
+
type: "button",
|
|
1207
|
+
className: `fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`,
|
|
1208
|
+
role: "radio",
|
|
1209
|
+
"aria-label": String(n),
|
|
1210
|
+
"aria-checked": value === n,
|
|
1211
|
+
tabIndex: i === tabbable ? 0 : -1,
|
|
1212
|
+
onClick: () => setValue(value === n ? null : n),
|
|
1213
|
+
children: n
|
|
1214
|
+
},
|
|
1215
|
+
n
|
|
1216
|
+
))
|
|
1217
|
+
}
|
|
1218
|
+
),
|
|
1219
|
+
hasLabels && /* @__PURE__ */ jsxs4("div", { className: "fillo-scale-labels", children: [
|
|
1220
|
+
/* @__PURE__ */ jsx4("span", { id: minLabelId, children: scale.minLabel }),
|
|
1221
|
+
/* @__PURE__ */ jsx4("span", { id: maxLabelId, children: scale.maxLabel })
|
|
1059
1222
|
] })
|
|
1060
1223
|
] });
|
|
1061
1224
|
}
|
|
@@ -1117,20 +1280,30 @@ function Matrix({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1117
1280
|
/* @__PURE__ */ jsx4("th", {}),
|
|
1118
1281
|
matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
|
|
1119
1282
|
] }) }),
|
|
1120
|
-
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) =>
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
"
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1283
|
+
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
|
|
1284
|
+
const rowLabelId = `${ids.inputId}-${row.id}`;
|
|
1285
|
+
const answered = answers[row.id] !== void 0;
|
|
1286
|
+
const rowInvalid = Boolean(error) && field.required && !answered;
|
|
1287
|
+
return (
|
|
1288
|
+
// Each row is its own radiogroup, labelled by the row question.
|
|
1289
|
+
/* @__PURE__ */ jsxs4("tr", { role: "radiogroup", "aria-labelledby": rowLabelId, children: [
|
|
1290
|
+
/* @__PURE__ */ jsx4("th", { scope: "row", id: rowLabelId, children: row.label }),
|
|
1291
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
|
|
1292
|
+
"input",
|
|
1293
|
+
{
|
|
1294
|
+
type: "radio",
|
|
1295
|
+
className: "fillo-option-input",
|
|
1296
|
+
name: `${ids.name}-${row.id}`,
|
|
1297
|
+
"aria-label": `${row.label}: ${col.label}`,
|
|
1298
|
+
"aria-required": field.required ? true : void 0,
|
|
1299
|
+
"aria-invalid": rowInvalid ? true : void 0,
|
|
1300
|
+
checked: answers[row.id] === col.id,
|
|
1301
|
+
onChange: () => setValue({ ...answers, [row.id]: col.id })
|
|
1302
|
+
}
|
|
1303
|
+
) }, col.id))
|
|
1304
|
+
] }, row.id)
|
|
1305
|
+
);
|
|
1306
|
+
}) })
|
|
1134
1307
|
] }) }) });
|
|
1135
1308
|
}
|
|
1136
1309
|
function Hidden() {
|
|
@@ -1183,14 +1356,42 @@ function shouldAutoSubmit(field, value, api) {
|
|
|
1183
1356
|
return false;
|
|
1184
1357
|
}
|
|
1185
1358
|
})();
|
|
1186
|
-
|
|
1359
|
+
if (!hasDiscreteAnswer) return false;
|
|
1360
|
+
if (value === api.data[field.id]) return false;
|
|
1361
|
+
const nextData = { ...api.data, [field.id]: value };
|
|
1362
|
+
const visible = visibleFields(api.form, nextData);
|
|
1363
|
+
if (visible.length !== 1 || visible[0]?.id !== field.id) return false;
|
|
1364
|
+
return validateResponse(api.form, nextData).ok;
|
|
1187
1365
|
}
|
|
1188
|
-
function
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1366
|
+
function blockHasPiping(block) {
|
|
1367
|
+
const has = (s) => typeof s === "string" && s.includes("{{");
|
|
1368
|
+
switch (block.kind) {
|
|
1369
|
+
case "heading":
|
|
1370
|
+
case "paragraph":
|
|
1371
|
+
return has(block.text);
|
|
1372
|
+
case "divider":
|
|
1373
|
+
return false;
|
|
1374
|
+
default:
|
|
1375
|
+
return has(block.label) || has(block.description);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function blockPropsEqual(prev, next) {
|
|
1379
|
+
if (prev.block !== next.block || prev.components !== next.components || prev.customComponents !== next.customComponents) {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1382
|
+
const id = next.block.id;
|
|
1383
|
+
if (prev.api.data[id] !== next.api.data[id]) return false;
|
|
1384
|
+
if (prev.api.errors[id] !== next.api.errors[id]) return false;
|
|
1385
|
+
const needsWiderState = next.block.kind === "custom" || blockHasPiping(prev.block) || blockHasPiping(next.block);
|
|
1386
|
+
if (needsWiderState && prev.api.data !== next.api.data) return false;
|
|
1387
|
+
if (next.api.form.settings.submitMode === "auto") {
|
|
1388
|
+
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) {
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
function BlockRendererInner({ block, api, components, customComponents }) {
|
|
1194
1395
|
block = pipeBlock(block, api.data, api.form);
|
|
1195
1396
|
const ids = useFilloFieldIds(block.id);
|
|
1196
1397
|
if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
|
|
@@ -1224,6 +1425,7 @@ function BlockRenderer({
|
|
|
1224
1425
|
}
|
|
1225
1426
|
);
|
|
1226
1427
|
}
|
|
1428
|
+
var BlockRenderer = memo(BlockRendererInner, blockPropsEqual);
|
|
1227
1429
|
function FormField({
|
|
1228
1430
|
id,
|
|
1229
1431
|
components,
|
|
@@ -1276,15 +1478,26 @@ function badgeUrl(href) {
|
|
|
1276
1478
|
url.searchParams.set("utm_source", "form_badge");
|
|
1277
1479
|
return url.toString();
|
|
1278
1480
|
}
|
|
1481
|
+
function focusFirstInvalid(formEl) {
|
|
1482
|
+
if (!formEl || typeof window === "undefined") return;
|
|
1483
|
+
const run = () => {
|
|
1484
|
+
const target = formEl.querySelector('[aria-invalid="true"]');
|
|
1485
|
+
target?.focus?.();
|
|
1486
|
+
};
|
|
1487
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
1488
|
+
else setTimeout(run, 0);
|
|
1489
|
+
}
|
|
1279
1490
|
function isAutoSubmitBlock(block) {
|
|
1280
1491
|
return isField2(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
1281
1492
|
}
|
|
1282
|
-
function needsExplicitSubmit(
|
|
1283
|
-
|
|
1493
|
+
function needsExplicitSubmit(visible) {
|
|
1494
|
+
if (visible.length === 0) return false;
|
|
1495
|
+
if (visible.length > 1) return true;
|
|
1496
|
+
return !isAutoSubmitBlock(visible[0]);
|
|
1284
1497
|
}
|
|
1285
1498
|
function FilloForm(props) {
|
|
1286
1499
|
const { form, formId } = props;
|
|
1287
|
-
const client =
|
|
1500
|
+
const client = useMemo4(
|
|
1288
1501
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
1289
1502
|
[props.client, formId]
|
|
1290
1503
|
);
|
|
@@ -1320,7 +1533,7 @@ function FilloForm(props) {
|
|
|
1320
1533
|
};
|
|
1321
1534
|
}, [needsFetch, client, formId, onError]);
|
|
1322
1535
|
const rawSchema = inlineSchema ?? fetched;
|
|
1323
|
-
const normalized =
|
|
1536
|
+
const normalized = useMemo4(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1324
1537
|
const schema = normalized?.ok ? normalized.schema : null;
|
|
1325
1538
|
const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
|
|
1326
1539
|
if (normalized && !normalized.ok) {
|
|
@@ -1396,7 +1609,7 @@ function ResolvedForm(props) {
|
|
|
1396
1609
|
skipValidation: props.skipValidation,
|
|
1397
1610
|
getHoneypot: () => hpRef.current
|
|
1398
1611
|
});
|
|
1399
|
-
const themed =
|
|
1612
|
+
const themed = useMemo4(() => themeProps(theme), [theme]);
|
|
1400
1613
|
const settings = schema.settings;
|
|
1401
1614
|
const submitted = api.status === "submitted";
|
|
1402
1615
|
useEffect7(() => {
|
|
@@ -1416,7 +1629,7 @@ function ResolvedForm(props) {
|
|
|
1416
1629
|
}
|
|
1417
1630
|
const multiPage = api.pageCount > 1;
|
|
1418
1631
|
const autoSubmit = settings.submitMode === "auto";
|
|
1419
|
-
const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(api.
|
|
1632
|
+
const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields2(api.form, api.data));
|
|
1420
1633
|
const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
|
|
1421
1634
|
return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
|
|
1422
1635
|
"form",
|
|
@@ -1426,8 +1639,12 @@ function ResolvedForm(props) {
|
|
|
1426
1639
|
noValidate: true,
|
|
1427
1640
|
onSubmit: (e) => {
|
|
1428
1641
|
e.preventDefault();
|
|
1429
|
-
|
|
1430
|
-
|
|
1642
|
+
const formEl = e.currentTarget;
|
|
1643
|
+
if (api.isLastPage) void api.submit().then(() => focusFirstInvalid(formEl));
|
|
1644
|
+
else {
|
|
1645
|
+
api.next();
|
|
1646
|
+
focusFirstInvalid(formEl);
|
|
1647
|
+
}
|
|
1431
1648
|
},
|
|
1432
1649
|
children: [
|
|
1433
1650
|
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
@@ -1498,15 +1715,15 @@ function ResolvedForm(props) {
|
|
|
1498
1715
|
}
|
|
1499
1716
|
|
|
1500
1717
|
// src/provider.tsx
|
|
1501
|
-
import { useId as useId3, useMemo as
|
|
1718
|
+
import { useId as useId3, useMemo as useMemo5 } from "react";
|
|
1502
1719
|
import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2 } from "@usefillo/core";
|
|
1503
1720
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1504
1721
|
function FilloProvider({ children, form, formId, client, ...options }) {
|
|
1505
1722
|
const instanceId = useId3().replace(/:/g, "");
|
|
1506
1723
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1507
1724
|
const rawSchema = codeForm ? codeForm.schema : form;
|
|
1508
|
-
const schemaKey = JSON.stringify(rawSchema);
|
|
1509
|
-
const normalized =
|
|
1725
|
+
const schemaKey = useMemo5(() => JSON.stringify(rawSchema), [rawSchema]);
|
|
1726
|
+
const normalized = useMemo5(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
|
|
1510
1727
|
if (!normalized.ok) {
|
|
1511
1728
|
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1512
1729
|
}
|
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.1",
|
|
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",
|