@usefillo/react 0.10.0 → 0.11.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.
@@ -4,7 +4,7 @@
4
4
  * so you can theme with variables, override classes, or skip this file and
5
5
  * style from scratch.
6
6
  *
7
- * SYNC: packages/dom/src/styles.css is a byte-for-byte copy of this file
7
+ * SYNC: packages/dom/src/styles.css is byte-for-byte identical to this file
8
8
  * (@usefillo/dom builds its published CSS from its own copy via
9
9
  * scripts/build-css.mjs --src src/styles.css). Edit BOTH together.
10
10
  */
@@ -20,6 +20,7 @@
20
20
 
21
21
 
22
22
  .fillo-form {
23
+ color-scheme: light;
23
24
  --fillo-primary: #18181b;
24
25
  --fillo-primary-contrast: #ffffff;
25
26
  --fillo-bg: transparent;
@@ -46,6 +47,7 @@
46
47
  }
47
48
 
48
49
  .fillo-form[data-fillo-color-scheme="dark"] {
50
+ color-scheme: dark;
49
51
  --fillo-primary: #fafafa;
50
52
  --fillo-primary-contrast: #18181b;
51
53
  --fillo-text: #f4f4f5;
@@ -59,7 +61,8 @@
59
61
 
60
62
  @media (prefers-color-scheme: dark) {
61
63
  .fillo-form:not([data-fillo-color-scheme="light"]):not([data-fillo-has-color-theme]) {
62
- --fillo-primary: #fafafa;
64
+ color-scheme: dark;
65
+ --fillo-primary: #fafafa;
63
66
  --fillo-primary-contrast: #18181b;
64
67
  --fillo-text: #f4f4f5;
65
68
  --fillo-muted: #a1a1aa;
@@ -110,6 +113,38 @@
110
113
  :where(.fillo-field--error) .fillo-input { border-color: var(--fillo-error); }
111
114
  .fillo-textarea { resize: vertical; min-height: 6rem; }
112
115
 
116
+ /* Number field adornments: the wrapper draws the field's box (border/
117
+ radius/focus, mirrors .fillo-input above) and the nested input goes
118
+ borderless so prefix/suffix read as inside one control, not stacked
119
+ beside it like the phone field's country picker. */
120
+ .fillo-number {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.3rem;
124
+ box-sizing: border-box;
125
+ min-height: 2.5rem;
126
+ background: var(--fillo-control-bg);
127
+ border: 1px solid var(--fillo-border);
128
+ border-radius: var(--fillo-radius);
129
+ padding: 0.6rem 0.8rem;
130
+ transition: border-color 120ms ease, box-shadow 120ms ease;
131
+ }
132
+ .fillo-number:focus-within {
133
+ border-color: var(--fillo-primary);
134
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
135
+ }
136
+ :where(.fillo-field--error) .fillo-number { border-color: var(--fillo-error); }
137
+ :where(.fillo-number) .fillo-input {
138
+ flex: 1 1 auto;
139
+ min-width: 0;
140
+ min-height: 0;
141
+ border: none;
142
+ background: transparent;
143
+ padding: 0;
144
+ }
145
+ :where(.fillo-number) .fillo-input:focus-visible { box-shadow: none; }
146
+ .fillo-number-prefix, .fillo-number-suffix { flex: 0 0 auto; color: var(--fillo-muted); }
147
+
113
148
  /* Phone field: country picker + national input */
114
149
  .fillo-phone { position: relative; display: flex; align-items: stretch; gap: 0.4rem; }
115
150
  .fillo-phone-country { position: relative; flex: 0 0 auto; }
@@ -132,7 +167,7 @@
132
167
  .fillo-phone-input { flex: 1 1 auto; min-width: 0; }
133
168
  :where(.fillo-field--error) .fillo-phone-flag { border-color: var(--fillo-error); }
134
169
  .fillo-phone-popover {
135
- position: absolute; z-index: 30; top: calc(100% + 0.3rem); left: 0;
170
+ position: absolute; z-index: 30; top: calc(100% + 0.3rem); inset-inline-start: 0;
136
171
  width: min(18rem, calc(100vw - 1rem));
137
172
  max-width: var(--fillo-phone-popover-max-width, 84vw);
138
173
  max-height: min(var(--fillo-phone-popover-max-height, 20rem), calc(100vh - 1rem));
@@ -171,13 +206,13 @@
171
206
  }
172
207
  .fillo-select {
173
208
  cursor: pointer;
174
- padding-right: 2.45rem;
209
+ padding-inline-end: 2.45rem;
175
210
  }
176
211
  .fillo-select-icon {
177
212
  pointer-events: none;
178
213
  position: absolute;
179
214
  top: 50%;
180
- right: 0.9rem;
215
+ inset-inline-end: 0.9rem;
181
216
  width: 0.62rem;
182
217
  height: 0.62rem;
183
218
  border-right: 1.5px solid currentColor;
@@ -235,6 +270,9 @@
235
270
  transition: color 120ms ease;
236
271
  }
237
272
  :where(.fillo-option--selected) .fillo-option-icon { color: var(--fillo-primary); }
273
+ /* Selected marker for icon-mode options — the non-color signal (the native
274
+ input is visually hidden there, so forced-colors has nothing else). */
275
+ .fillo-option-check { flex: 0 0 auto; margin-inline-start: auto; color: var(--fillo-primary); }
238
276
  .fillo-option-label { font-size: 0.95rem; }
239
277
 
240
278
  .fillo-option-main {
@@ -292,7 +330,7 @@
292
330
  .fillo-toggle-thumb {
293
331
  width: 1rem;
294
332
  height: 1rem;
295
- margin-left: 0.18rem;
333
+ margin-inline-start: 0.18rem;
296
334
  border-radius: 999px;
297
335
  background: var(--fillo-control-bg);
298
336
  box-shadow: 0 1px 2px color-mix(in srgb, var(--fillo-text) 18%, transparent);
@@ -306,6 +344,14 @@
306
344
  transform: translateX(1rem);
307
345
  background: var(--fillo-primary-contrast);
308
346
  }
347
+ /* RTL: the thumb travels toward the track's other edge. Deviates from the
348
+ spec's literal selector text, which left three fillo-* classes bare
349
+ outside :where() and would fail this file's own specificity lint
350
+ (styles-lint.test.mjs) — wrapped each qualifier individually instead,
351
+ mirroring the checked+track rule directly above. */
352
+ :where([dir="rtl"]) :where(.fillo-toggle-input:checked) + :where(.fillo-toggle-track) .fillo-toggle-thumb {
353
+ transform: translateX(-1rem);
354
+ }
309
355
  :where(.fillo-toggle-input:focus-visible) + .fillo-toggle-track {
310
356
  box-shadow:
311
357
  inset 0 0 0 1px var(--fillo-primary),
@@ -333,22 +379,18 @@
333
379
  /* ---------- Rating ---------- */
334
380
  .fillo-rating { display: flex; gap: 0.25rem; }
335
381
  .fillo-star {
336
- font-size: 1.6rem;
382
+ font-size: 1.75rem;
337
383
  line-height: 1;
338
384
  background: none;
339
385
  border: none;
340
386
  cursor: pointer;
341
- color: var(--fillo-border);
387
+ color: var(--fillo-muted);
342
388
  padding: 0.1rem;
343
389
  transition: color 100ms ease, transform 100ms ease;
344
390
  }
345
391
  .fillo-star:hover { transform: scale(1.15); }
346
392
  .fillo-star--active { color: var(--fillo-primary); }
347
- .fillo-star:focus-visible {
348
- outline: none;
349
- border-radius: calc(var(--fillo-radius) * 0.5);
350
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 30%, transparent);
351
- }
393
+ /* :focus-visible treatment standardized below (P1.1 solid-outline block). */
352
394
 
353
395
  /* ---------- Linear scale ---------- */
354
396
  /* Steps share one row and shrink to fit — an 11-step NPS scale must never
@@ -423,7 +465,9 @@
423
465
  transition: color 120ms ease, border-color 120ms ease;
424
466
  }
425
467
  .fillo-ranking-move:hover:not(:disabled) { color: var(--fillo-primary); border-color: var(--fillo-primary); }
426
- .fillo-ranking-move:disabled { opacity: 0.35; cursor: default; }
468
+ /* opacity set below (P1.3 block, raised to 0.5 for the internal 3:1
469
+ disabled-contrast advisory); cursor stays here. */
470
+ .fillo-ranking-move:disabled { cursor: default; }
427
471
 
428
472
  /* ---------- Matrix ---------- */
429
473
  .fillo-matrix-wrap { overflow-x: auto; }
@@ -434,9 +478,21 @@
434
478
  border-bottom: 1px solid var(--fillo-border);
435
479
  }
436
480
  .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.8rem; }
437
- .fillo-matrix tbody th { text-align: left; font-weight: 500; }
481
+ .fillo-matrix tbody th { text-align: start; font-weight: 500; }
438
482
  .fillo-matrix tbody tr:last-child th, .fillo-matrix tbody tr:last-child td { border-bottom: none; }
439
483
 
484
+ /* ---------- Calculated ---------- */
485
+ /* A quiet computed-value line, not a control: no border or input chrome —
486
+ the slightly heavier tabular number is the only emphasis. */
487
+ .fillo-calculated-value {
488
+ font-size: 1.05rem;
489
+ font-weight: 600;
490
+ font-variant-numeric: tabular-nums;
491
+ color: var(--fillo-text);
492
+ }
493
+ /* Unanswered (an incomplete calculation renders an em dash). */
494
+ .fillo-calculated-value--empty { color: var(--fillo-muted); font-weight: 400; }
495
+
440
496
  /* ---------- Signature ---------- */
441
497
  .fillo-signature {
442
498
  position: relative;
@@ -464,7 +520,7 @@
464
520
  .fillo-signature-clear {
465
521
  position: absolute;
466
522
  top: 0.4rem;
467
- right: 0.4rem;
523
+ inset-inline-end: 0.4rem;
468
524
  font: inherit;
469
525
  font-size: 0.75rem;
470
526
  border: 1px solid var(--fillo-border);
@@ -531,14 +587,29 @@
531
587
  font-size: 0.88rem;
532
588
  background: var(--fillo-control-bg);
533
589
  }
534
- .fillo-file--failed { border-color: var(--fillo-error); }
590
+ .fillo-file--failed {
591
+ grid-template-columns: minmax(0, 1fr) auto;
592
+ align-items: start;
593
+ border-color: var(--fillo-error);
594
+ }
535
595
  .fillo-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
536
596
  .fillo-file-meta { color: var(--fillo-muted); font-variant-numeric: tabular-nums; }
597
+ .fillo-file-actions { display: inline-flex; align-items: center; gap: 0.45rem; }
598
+ .fillo-file-error { grid-column: 1 / -1; color: var(--fillo-error); font-size: 0.82rem; line-height: 1.35; }
599
+ .fillo-file-retry { border: 0; padding: 0; background: none; color: var(--fillo-primary); cursor: pointer; font: inherit; font-weight: 550; }
600
+ .fillo-file-retry:hover { text-decoration: underline; }
537
601
  .fillo-file-remove {
538
602
  background: none; border: none; cursor: pointer;
539
603
  color: var(--fillo-muted); font-size: 1.1rem; line-height: 1; padding: 0 0.2rem;
540
604
  }
541
605
  .fillo-file-remove:hover { color: var(--fillo-error); }
606
+ /* Same reset as .fillo-file-remove above (was completely unstyled, audit
607
+ P2.14) — primary on hover, not error, since retry is constructive. */
608
+ .fillo-file-retry {
609
+ background: none; border: none; cursor: pointer;
610
+ color: var(--fillo-muted); font: inherit; font-weight: 500; padding: 0 0.2rem;
611
+ }
612
+ .fillo-file-retry:hover { color: var(--fillo-primary); }
542
613
  .fillo-progress {
543
614
  grid-column: 1 / -1;
544
615
  height: 4px;
@@ -572,10 +643,7 @@
572
643
  }
573
644
  .fillo-button:active { transform: translateY(1px); }
574
645
  .fillo-button:disabled { opacity: 0.55; cursor: not-allowed; }
575
- .fillo-button:focus-visible {
576
- outline: none;
577
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 30%, transparent);
578
- }
646
+ /* :focus-visible treatment standardized below (P1.1 solid-outline block). */
579
647
  .fillo-button--primary { background: var(--fillo-primary); color: var(--fillo-primary-contrast); }
580
648
  .fillo-button--ghost { background: transparent; color: var(--fillo-text); border-color: var(--fillo-border); }
581
649
 
@@ -602,15 +670,14 @@
602
670
  margin: 0;
603
671
  }
604
672
 
605
- /* ---------- Not-open overlay (draft / not-accepting, production) ---------- */
606
- /* The real form renders blurred and display-only beneath a small card —
607
- honest about the state without a dead grey panel, impossible to fill. */
673
+ /* ---------- Not-open state (draft / not-accepting, production) ---------- */
674
+ /* Keep the display-only preview in the DOM as a safety fallback for hosts
675
+ that skip the defaults, but present a calm empty state when this stylesheet
676
+ is loaded. Respondents should not have to read a blurred form to understand
677
+ that it is unavailable. */
608
678
  .fillo-form--not-open { position: relative; }
609
679
  .fillo-not-open-preview {
610
- filter: blur(3px) saturate(0.6);
611
- opacity: 0.85;
612
- pointer-events: none;
613
- user-select: none;
680
+ display: none;
614
681
  }
615
682
  /* The preview's controls sit in one natively disabled fieldset; keep it
616
683
  invisible in the layout (it inherits the form's own column gap). */
@@ -624,34 +691,39 @@
624
691
  min-width: 0;
625
692
  }
626
693
  .fillo-not-open-card {
627
- position: absolute;
628
- left: 50%;
629
- top: 50%;
630
- transform: translate(-50%, -50%);
631
- z-index: 1;
632
694
  display: flex;
633
695
  flex-direction: column;
696
+ align-items: center;
634
697
  gap: 0.4rem;
635
- width: min(20rem, calc(100% - 2rem));
636
- padding: 1.4rem 1.6rem;
698
+ width: 100%;
699
+ box-sizing: border-box;
700
+ padding: 2rem 1rem;
637
701
  text-align: center;
638
- border: 1px solid var(--fillo-border);
639
- border-radius: var(--fillo-radius);
640
- background: var(--fillo-control-bg);
641
- box-shadow: 0 12px 32px -14px rgba(0, 0, 0, 0.35);
642
702
  }
643
- .fillo-not-open-title { margin: 0; font-size: 1.05rem; letter-spacing: 0; }
644
- .fillo-not-open-body { margin: 0; font-size: 0.875rem; color: var(--fillo-muted); line-height: 1.5; }
703
+ .fillo-not-open-title { margin: 0; font-size: 1.05rem; font-weight: 650; letter-spacing: 0; }
704
+ .fillo-not-open-body {
705
+ max-width: 26rem;
706
+ margin: 0;
707
+ font-size: 0.875rem;
708
+ color: var(--fillo-muted);
709
+ line-height: 1.5;
710
+ }
645
711
 
646
712
  /* ---------- Success ---------- */
647
713
  .fillo-success { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; text-align: center; padding: 1.4rem 1rem; }
648
714
  .fillo-success-mark {
649
- width: 2rem; height: 2rem;
715
+ width: 2.25rem; height: 2.25rem;
650
716
  display: grid; place-items: center;
651
717
  border-radius: 999px;
652
- background: color-mix(in srgb, var(--fillo-primary) 10%, transparent);
653
- color: var(--fillo-primary);
654
- font-size: 0.95rem; font-weight: 700;
718
+ border: 1px solid color-mix(in srgb, var(--fillo-primary) 18%, transparent);
719
+ background: color-mix(in srgb, var(--fillo-primary) 8%, transparent);
720
+ }
721
+ .fillo-success-mark::before {
722
+ content: "";
723
+ width: 0.65rem; height: 0.35rem;
724
+ border-left: 2px solid var(--fillo-primary);
725
+ border-bottom: 2px solid var(--fillo-primary);
726
+ transform: translateY(-0.08rem) rotate(-45deg);
655
727
  }
656
728
  .fillo-success-title { margin: 0; font-size: 1.05rem; letter-spacing: 0; }
657
729
  .fillo-success-message { margin: 0; font-size: 0.875rem; color: var(--fillo-muted); }
@@ -785,11 +857,7 @@
785
857
  text-underline-offset: 2px;
786
858
  white-space: nowrap;
787
859
  }
788
- .fillo-resume-clear:focus-visible {
789
- outline: none;
790
- border-radius: 4px;
791
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
792
- }
860
+ /* :focus-visible treatment standardized below (P1.1 solid-outline block). */
793
861
 
794
862
  /* ---------- Human-verification challenge (Turnstile) ---------- */
795
863
  .fillo-turnstile-slot {
@@ -808,3 +876,99 @@
808
876
  font-size: 0.82rem;
809
877
  line-height: 1.45;
810
878
  }
879
+
880
+ /* Error summary (GOV.UK pattern): ONE alert on failed submit; links focus
881
+ their fields. Per-field messages are plain describedby text. */
882
+ .fillo-error-summary {
883
+ border: 2px solid var(--fillo-error);
884
+ border-radius: var(--fillo-radius);
885
+ padding: 0.8rem 1rem;
886
+ }
887
+ .fillo-error-summary-title { margin: 0 0 0.4rem; font-size: 1rem; font-weight: 600; }
888
+ .fillo-error-summary ul { margin: 0; padding-inline-start: 1.1rem; }
889
+ .fillo-error-summary li { margin: 0.2rem 0; }
890
+ .fillo-error-summary a { color: var(--fillo-error); }
891
+
892
+ /* ---------- Accessibility: focus rings, pointer targets, forced-colors
893
+ (input-quality audit P1.1-P1.3) ---------- */
894
+ /* Focus visibility: controls without a compensating border swap get a
895
+ solid ring — translucent box-shadows compute under 3:1 (WCAG 1.4.11)
896
+ and vanish in forced-colors. */
897
+ .fillo-star:focus-visible,
898
+ .fillo-button:focus-visible,
899
+ .fillo-resume-clear:focus-visible,
900
+ .fillo-ranking-move:focus-visible,
901
+ .fillo-file-remove:focus-visible,
902
+ .fillo-file-retry:focus-visible,
903
+ .fillo-signature-clear:focus-visible {
904
+ outline: 2px solid var(--fillo-primary);
905
+ outline-offset: 2px;
906
+ box-shadow: none;
907
+ }
908
+
909
+ /* Minimum pointer targets (WCAG 2.2 AA 24px; 1.5rem = 24px @16px root). */
910
+ .fillo-ranking-move,
911
+ .fillo-file-remove,
912
+ .fillo-file-retry,
913
+ .fillo-signature-clear {
914
+ min-width: 1.5rem;
915
+ min-height: 1.5rem;
916
+ display: inline-flex;
917
+ align-items: center;
918
+ justify-content: center;
919
+ }
920
+ .fillo-star { min-width: 1.75rem; min-height: 1.75rem; }
921
+ .fillo-matrix-cell {
922
+ display: flex;
923
+ align-items: center;
924
+ justify-content: center;
925
+ min-height: 1.5rem;
926
+ padding: 0.35rem;
927
+ cursor: pointer;
928
+ }
929
+
930
+ /* Placeholder joins the token system (was UA-default, ungoverned). */
931
+ .fillo-input::placeholder,
932
+ .fillo-phone-search::placeholder { color: var(--fillo-muted); }
933
+
934
+ /* Ranking disabled state: WCAG-exempt, but keep it findable (contract's
935
+ internal 3:1 advisory). */
936
+ .fillo-ranking-move:disabled { opacity: 0.5; }
937
+
938
+ /* Matrix on narrow viewports: stacked rows, never a horizontal scroll
939
+ trap. Column names come from data-label (set by both renderers). */
940
+ @media (max-width: 30rem) {
941
+ .fillo-matrix thead { position: absolute; width: 1px; height: 1px;
942
+ overflow: hidden; clip-path: inset(50%); }
943
+ .fillo-matrix, .fillo-matrix tbody, .fillo-matrix tr,
944
+ .fillo-matrix th, .fillo-matrix td { display: block; width: 100%; }
945
+ .fillo-matrix tbody th { padding: 0.55rem 0 0.15rem; }
946
+ .fillo-matrix td { display: flex; align-items: center; gap: 0.5rem; }
947
+ .fillo-matrix td::before { content: attr(data-label);
948
+ flex: 1 1 auto; font-size: 0.85em; }
949
+ /* Deviates from the spec's literal ".fillo-matrix .fillo-matrix-cell"
950
+ text (two bare fillo-* classes, no :where()) — that fails this file's
951
+ own specificity lint (styles-lint.test.mjs: "wrap qualifiers in
952
+ :where()"). Wrapped the ancestor qualifier instead. */
953
+ :where(.fillo-matrix) .fillo-matrix-cell { flex: 0 0 auto; }
954
+ }
955
+
956
+ /* Forced-colors: restore what author colors can't carry. */
957
+ @media (forced-colors: active) {
958
+ .fillo-star--active,
959
+ .fillo-scale-step--active,
960
+ :where(.fillo-option--selected) {
961
+ outline: 2px solid SelectedItem;
962
+ outline-offset: -2px;
963
+ }
964
+ .fillo-input:focus-visible,
965
+ .fillo-number:focus-within,
966
+ .fillo-phone-flag:focus-visible,
967
+ .fillo-phone-search:focus-visible,
968
+ .fillo-scale-step:focus-visible,
969
+ .fillo-dropzone:focus-visible,
970
+ .fillo-select:focus-visible {
971
+ outline: 2px solid Highlight;
972
+ outline-offset: 2px;
973
+ }
974
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/react",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Headless React components for embedding Fillo forms natively in your product.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@usefillo/core": "^0.10.0"
37
+ "@usefillo/core": "^0.11.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^18.0.0 || ^19.0.0",
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/react": "^19.1.0",
45
+ "axe-core": "^4.10.0",
45
46
  "react": "^19.1.0",
46
47
  "tsup": "^8.4.0",
47
48
  "typescript": "^5.8.3",