@usefillo/react 0.12.0 → 0.12.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.js CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  createClient,
16
16
  formSchemasEqual,
17
17
  isBuildTimeDevEnv as isBuildTimeDevEnv2,
18
- isField as isField2,
19
18
  isFilloError as isFilloError2,
20
19
  isLikelyDevEnv,
21
20
  needsExplicitSubmit,
@@ -25,7 +24,7 @@ import {
25
24
  reachablePageSequence,
26
25
  resolveThemeAppearance
27
26
  } from "@usefillo/core";
28
- import { REQUIRED_FIELD_MESSAGE as REQUIRED_FIELD_MESSAGE2, resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
27
+ import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
29
28
 
30
29
  // src/appearance.tsx
31
30
  import { createContext, useContext } from "react";
@@ -488,6 +487,7 @@ import {
488
487
  pipeBlock,
489
488
  radioGroupStep,
490
489
  REQUIRED_FIELD_MESSAGE,
490
+ requiredFieldMessage,
491
491
  shouldAutoSubmit,
492
492
  slotClass as slotClass2,
493
493
  visibleFields
@@ -1008,6 +1008,76 @@ function uploadFailureMessage(error, strings) {
1008
1008
  }
1009
1009
  return strings.uploadFailed;
1010
1010
  }
1011
+ function FileStateIcon({ state }) {
1012
+ if (state === "done") {
1013
+ return /* @__PURE__ */ jsx4("span", { className: "fillo-file-state fillo-file-state--done", "aria-hidden": "true", children: /* @__PURE__ */ jsxs4(
1014
+ "svg",
1015
+ {
1016
+ className: "fillo-file-state-icon",
1017
+ viewBox: "0 0 24 24",
1018
+ width: "22",
1019
+ height: "22",
1020
+ fill: "none",
1021
+ "aria-hidden": "true",
1022
+ focusable: "false",
1023
+ children: [
1024
+ /* @__PURE__ */ jsx4("path", { d: "M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0" }),
1025
+ /* @__PURE__ */ jsx4("path", { d: "m8.5 12 2.25 2.25 4.75-5" })
1026
+ ]
1027
+ }
1028
+ ) });
1029
+ }
1030
+ if (state === "failed") {
1031
+ return /* @__PURE__ */ jsx4("span", { className: "fillo-file-state fillo-file-state--failed", "aria-hidden": "true", children: /* @__PURE__ */ jsxs4(
1032
+ "svg",
1033
+ {
1034
+ className: "fillo-file-state-icon",
1035
+ viewBox: "0 0 24 24",
1036
+ width: "22",
1037
+ height: "22",
1038
+ fill: "none",
1039
+ "aria-hidden": "true",
1040
+ focusable: "false",
1041
+ children: [
1042
+ /* @__PURE__ */ jsx4("path", { d: "M3.75 12a8.25 8.25 0 1 0 16.5 0 8.25 8.25 0 1 0-16.5 0" }),
1043
+ /* @__PURE__ */ jsx4("path", { d: "M12 7.75v5.5" }),
1044
+ /* @__PURE__ */ jsx4("path", { d: "M12 16.5h.01" })
1045
+ ]
1046
+ }
1047
+ ) });
1048
+ }
1049
+ return /* @__PURE__ */ jsx4("span", { className: "fillo-file-state fillo-file-state--uploading", "aria-hidden": "true", children: /* @__PURE__ */ jsxs4(
1050
+ "svg",
1051
+ {
1052
+ className: "fillo-file-state-icon",
1053
+ viewBox: "0 0 24 24",
1054
+ width: "22",
1055
+ height: "22",
1056
+ fill: "none",
1057
+ "aria-hidden": "true",
1058
+ focusable: "false",
1059
+ children: [
1060
+ /* @__PURE__ */ jsx4("path", { d: "M7.25 3.75h6.5l3 3v13.5h-9.5z" }),
1061
+ /* @__PURE__ */ jsx4("path", { d: "M13.75 3.75v3h3" })
1062
+ ]
1063
+ }
1064
+ ) });
1065
+ }
1066
+ function CloseIcon() {
1067
+ return /* @__PURE__ */ jsx4(
1068
+ "svg",
1069
+ {
1070
+ className: "fillo-file-action-icon",
1071
+ viewBox: "0 0 24 24",
1072
+ width: "16",
1073
+ height: "16",
1074
+ fill: "none",
1075
+ "aria-hidden": "true",
1076
+ focusable: "false",
1077
+ children: /* @__PURE__ */ jsx4("path", { d: "m7 7 10 10M17 7 7 17" })
1078
+ }
1079
+ );
1080
+ }
1011
1081
  function FileUploadField({
1012
1082
  field,
1013
1083
  value,
@@ -1197,90 +1267,91 @@ function FileUploadField({
1197
1267
  ),
1198
1268
  (inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs4("ul", { className: "fillo-files", children: [
1199
1269
  files.map((f) => /* @__PURE__ */ jsxs4("li", { className: "fillo-file fillo-file--done", children: [
1200
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1201
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
1202
- /* @__PURE__ */ jsx4("span", { className: "fillo-sr-only", children: "Uploaded" }),
1203
- /* @__PURE__ */ jsx4(
1270
+ /* @__PURE__ */ jsx4(FileStateIcon, { state: "done" }),
1271
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-content", children: [
1272
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1273
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: strings.uploadedFile(formatBytes(f.size)) })
1274
+ ] }),
1275
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-actions", children: /* @__PURE__ */ jsx4(
1204
1276
  "button",
1205
1277
  {
1206
1278
  type: "button",
1207
1279
  className: "fillo-file-remove",
1208
- "aria-label": `Remove ${f.name}`,
1280
+ "aria-label": `${strings.uploadRemove} ${f.name}`,
1209
1281
  onClick: () => removeFile(f.fileId),
1210
- children: "\xD7"
1282
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
1211
1283
  }
1212
- )
1284
+ ) })
1213
1285
  ] }, f.fileId)),
1214
- inFlight.map((f) => /* @__PURE__ */ jsxs4(
1215
- "li",
1216
- {
1217
- className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
1218
- ...f.error ? {} : {
1219
- role: "progressbar",
1220
- "aria-label": `Uploading ${f.name}`,
1221
- "aria-valuemin": 0,
1222
- "aria-valuemax": 100,
1223
- "aria-valuenow": Math.round(f.fraction * 100)
1224
- },
1225
- children: [
1286
+ inFlight.map((f) => /* @__PURE__ */ jsxs4("li", { className: `fillo-file${f.error ? " fillo-file--failed" : ""}`, children: [
1287
+ /* @__PURE__ */ jsx4(FileStateIcon, { state: f.error ? "failed" : "uploading" }),
1288
+ f.error ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1289
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-content", children: [
1226
1290
  /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1227
- f.error ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1228
- /* @__PURE__ */ jsxs4("span", { className: "fillo-file-actions", children: [
1229
- f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx4(
1230
- "button",
1231
- {
1232
- type: "button",
1233
- className: "fillo-file-retry",
1234
- "aria-label": `${strings.uploadRetry} ${f.name}`,
1235
- onClick: () => {
1236
- const retryFile = f.file;
1237
- if (!retryFile) return;
1238
- setInFlight((prev) => prev.filter((x) => x.key !== f.key));
1239
- void startUpload(retryFile);
1240
- },
1241
- children: strings.uploadRetry
1242
- }
1243
- ),
1244
- /* @__PURE__ */ jsx4(
1245
- "button",
1246
- {
1247
- type: "button",
1248
- className: "fillo-file-remove",
1249
- "aria-label": "Dismiss",
1250
- onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
1251
- children: "\xD7"
1252
- }
1253
- )
1254
- ] }),
1255
- /* @__PURE__ */ jsx4("span", { className: "fillo-file-error", role: "alert", children: f.error })
1256
- ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1257
- /* @__PURE__ */ jsxs4("span", { className: "fillo-file-meta", children: [
1258
- Math.round(f.fraction * 100),
1259
- "% of ",
1260
- formatBytes(f.size)
1261
- ] }),
1262
- /* @__PURE__ */ jsx4(
1263
- "button",
1264
- {
1265
- type: "button",
1266
- className: "fillo-file-remove",
1267
- "aria-label": `Cancel ${f.name}`,
1268
- onClick: () => cancelUpload(f.key),
1269
- children: "\xD7"
1270
- }
1271
- ),
1272
- /* @__PURE__ */ jsx4("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx4(
1291
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-error", role: "alert", children: f.error })
1292
+ ] }),
1293
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-actions", children: [
1294
+ f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx4(
1295
+ "button",
1296
+ {
1297
+ type: "button",
1298
+ className: "fillo-file-retry",
1299
+ "aria-label": `${strings.uploadRetry} ${f.name}`,
1300
+ onClick: () => {
1301
+ const retryFile = f.file;
1302
+ if (!retryFile) return;
1303
+ setInFlight((prev) => prev.filter((x) => x.key !== f.key));
1304
+ void startUpload(retryFile);
1305
+ },
1306
+ children: strings.uploadRetry
1307
+ }
1308
+ ),
1309
+ /* @__PURE__ */ jsx4(
1310
+ "button",
1311
+ {
1312
+ type: "button",
1313
+ className: "fillo-file-remove",
1314
+ "aria-label": `${strings.uploadDismiss} ${f.name}`,
1315
+ onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
1316
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
1317
+ }
1318
+ )
1319
+ ] })
1320
+ ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1321
+ /* @__PURE__ */ jsxs4("span", { className: "fillo-file-content", children: [
1322
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-name", children: f.name }),
1323
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-meta", children: strings.uploadingFile(Math.round(f.fraction * 100), formatBytes(f.size)) })
1324
+ ] }),
1325
+ /* @__PURE__ */ jsx4("span", { className: "fillo-file-actions", children: /* @__PURE__ */ jsx4(
1326
+ "button",
1327
+ {
1328
+ type: "button",
1329
+ className: "fillo-file-remove",
1330
+ "aria-label": `${strings.uploadCancel} ${f.name}`,
1331
+ onClick: () => cancelUpload(f.key),
1332
+ children: /* @__PURE__ */ jsx4(CloseIcon, {})
1333
+ }
1334
+ ) }),
1335
+ /* @__PURE__ */ jsx4(
1336
+ "span",
1337
+ {
1338
+ className: "fillo-progress",
1339
+ role: "progressbar",
1340
+ "aria-label": `Uploading ${f.name}`,
1341
+ "aria-valuemin": 0,
1342
+ "aria-valuemax": 100,
1343
+ "aria-valuenow": Math.round(f.fraction * 100),
1344
+ children: /* @__PURE__ */ jsx4(
1273
1345
  "span",
1274
1346
  {
1275
1347
  className: "fillo-progress-bar",
1276
1348
  style: { width: `${f.fraction * 100}%` }
1277
1349
  }
1278
- ) })
1279
- ] })
1280
- ]
1281
- },
1282
- f.key
1283
- ))
1350
+ )
1351
+ }
1352
+ )
1353
+ ] })
1354
+ ] }, f.key))
1284
1355
  ] }),
1285
1356
  error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, children: error })
1286
1357
  ]
@@ -2214,7 +2285,7 @@ function BlockRendererInner({ block, api, components, customComponents }) {
2214
2285
  Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
2215
2286
  }
2216
2287
  const rawError = api.errors[block.id];
2217
- const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
2288
+ const error = rawError === REQUIRED_FIELD_MESSAGE ? requiredFieldMessage(block, strings) : rawError;
2218
2289
  return /* @__PURE__ */ jsx5(
2219
2290
  Component,
2220
2291
  {
@@ -2414,16 +2485,13 @@ function nextFrame(run) {
2414
2485
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
2415
2486
  else setTimeout(run, 0);
2416
2487
  }
2417
- function focusErrorSummary(formEl) {
2488
+ function focusFirstInvalid(formEl) {
2418
2489
  if (!formEl || typeof window === "undefined") return;
2419
2490
  nextFrame(() => {
2420
- formEl.querySelector(".fillo-error-summary")?.focus();
2491
+ const invalid = formEl.querySelector('[aria-invalid="true"]');
2492
+ resolveFocusable(invalid)?.focus();
2421
2493
  });
2422
2494
  }
2423
- function focusField(inputId) {
2424
- if (typeof document === "undefined") return;
2425
- resolveFocusable(document.getElementById(inputId))?.focus();
2426
- }
2427
2495
  var HP_STYLE = {
2428
2496
  position: "absolute",
2429
2497
  left: "-9999px",
@@ -2986,17 +3054,6 @@ function ResolvedForm(props) {
2986
3054
  const progressPos = progressSeq.indexOf(api.page.id);
2987
3055
  const progressStep = progressPos >= 0 ? progressPos + 1 : Math.min(api.pageIndex + 1, progressTotal);
2988
3056
  const submitFixUrl = devChrome && resolutionFailureCode === "form_not_published" && warningCode === "storage_required" ? safeHttpUrl(warningUrl) : null;
2989
- const invalidFields = [];
2990
- for (const block of api.blocks) {
2991
- if (!isField2(block)) continue;
2992
- const raw = api.errors[block.id];
2993
- if (!raw) continue;
2994
- invalidFields.push({
2995
- field: block,
2996
- error: raw === REQUIRED_FIELD_MESSAGE2 ? strings.required : raw,
2997
- inputId: createFilloFieldIds(instanceId, block.id).inputId
2998
- });
2999
- }
3000
3057
  const rootClass = slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
3001
3058
  slot: "root",
3002
3059
  status: api.status
@@ -3101,24 +3158,6 @@ function ResolvedForm(props) {
3101
3158
  children: api.page.title
3102
3159
  }
3103
3160
  ),
3104
- invalidFields.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "fillo-error-summary", role: "alert", tabIndex: -1, "data-fillo": "error-summary", children: [
3105
- /* @__PURE__ */ jsx7("p", { className: "fillo-error-summary-title", children: strings.errorSummaryTitle }),
3106
- /* @__PURE__ */ jsx7("ul", { children: invalidFields.map(({ field, error, inputId }) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsxs6(
3107
- "a",
3108
- {
3109
- href: `#${inputId}`,
3110
- onClick: (e) => {
3111
- e.preventDefault();
3112
- focusField(inputId);
3113
- },
3114
- children: [
3115
- field.label,
3116
- ": ",
3117
- error
3118
- ]
3119
- }
3120
- ) }, field.id)) })
3121
- ] }),
3122
3161
  /* @__PURE__ */ jsx7(
3123
3162
  "div",
3124
3163
  {
@@ -3246,11 +3285,11 @@ function ResolvedForm(props) {
3246
3285
  e.preventDefault();
3247
3286
  const formEl = e.currentTarget;
3248
3287
  if (api.isLastPage) {
3249
- void api.submit().then(() => focusErrorSummary(formEl)).catch(() => {
3288
+ void api.submit().then(() => focusFirstInvalid(formEl)).catch(() => {
3250
3289
  });
3251
3290
  } else {
3252
3291
  api.next();
3253
- focusErrorSummary(formEl);
3292
+ focusFirstInvalid(formEl);
3254
3293
  }
3255
3294
  },
3256
3295
  children: formBody
package/dist/styles.css CHANGED
@@ -84,11 +84,12 @@
84
84
  /* ---------- Field chrome ---------- */
85
85
  .fillo-field { display: flex; flex-direction: column; gap: 0.45rem; }
86
86
  .fillo-label { font-weight: 550; font-size: 0.95rem; }
87
+ :where(.fillo-field--error) .fillo-label { color: var(--fillo-error); }
87
88
  /* Fillo marks the few optional fields rather than starring the many required
88
89
  ones — so a clean label reads as required by default. */
89
90
  .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.85em; }
90
91
  .fillo-description { color: var(--fillo-muted); font-size: 0.85rem; margin: 0; line-height: 1.5; }
91
- .fillo-error { color: var(--fillo-error); font-size: 0.85rem; margin: 0; }
92
+ .fillo-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; line-height: 1.4; margin: 0; }
92
93
  .fillo-submit-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; margin: 0; }
93
94
 
94
95
  /* ---------- Inputs ---------- */
@@ -111,6 +112,11 @@
111
112
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
112
113
  }
113
114
  :where(.fillo-field--error) .fillo-input { border-color: var(--fillo-error); }
115
+ :where(.fillo-field--error) .fillo-input:focus-visible {
116
+ border-color: var(--fillo-error);
117
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
118
+ outline: none;
119
+ }
114
120
  .fillo-textarea { resize: vertical; min-height: 6rem; }
115
121
 
116
122
  /* Number field adornments: the wrapper draws the field's box (border/
@@ -134,6 +140,11 @@
134
140
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
135
141
  }
136
142
  :where(.fillo-field--error) .fillo-number { border-color: var(--fillo-error); }
143
+ :where(.fillo-field--error) .fillo-number:focus-within {
144
+ border-color: var(--fillo-error);
145
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
146
+ outline: none;
147
+ }
137
148
  :where(.fillo-number) .fillo-input {
138
149
  flex: 1 1 auto;
139
150
  min-width: 0;
@@ -143,6 +154,7 @@
143
154
  padding: 0;
144
155
  }
145
156
  :where(.fillo-number) .fillo-input:focus-visible { box-shadow: none; }
157
+ :where(.fillo-field--error) :where(.fillo-number) .fillo-input:focus-visible { outline: none; }
146
158
  .fillo-number-prefix, .fillo-number-suffix { flex: 0 0 auto; color: var(--fillo-muted); }
147
159
 
148
160
  /* Phone field: country picker + national input */
@@ -242,6 +254,15 @@
242
254
  border-color: var(--fillo-primary);
243
255
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 14%, transparent);
244
256
  }
257
+ :where(.fillo-field--error) .fillo-option {
258
+ border-color: var(--fillo-error);
259
+ background: color-mix(in srgb, var(--fillo-error) 2%, var(--fillo-control-bg));
260
+ }
261
+ :where(.fillo-field--error) .fillo-option:focus-within {
262
+ border-color: var(--fillo-error);
263
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
264
+ outline: none;
265
+ }
245
266
  .fillo-option--selected {
246
267
  border-color: var(--fillo-primary);
247
268
  background: color-mix(in srgb, var(--fillo-primary) 6%, var(--fillo-control-bg));
@@ -357,6 +378,13 @@
357
378
  inset 0 0 0 1px var(--fillo-primary),
358
379
  0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
359
380
  }
381
+ :where(.fillo-field--error) .fillo-toggle { border-color: var(--fillo-error); }
382
+ :where(.fillo-field--error) :where(.fillo-toggle-input:focus-visible) + .fillo-toggle-track {
383
+ box-shadow:
384
+ inset 0 0 0 1px var(--fillo-error),
385
+ 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
386
+ outline: none;
387
+ }
360
388
  .fillo-toggle:where(:has(.fillo-toggle-input:checked)) {
361
389
  border-color: var(--fillo-primary);
362
390
  background: color-mix(in srgb, var(--fillo-primary) 5%, var(--fillo-control-bg));
@@ -416,6 +444,11 @@
416
444
  border-color: var(--fillo-primary);
417
445
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
418
446
  }
447
+ :where(.fillo-field--error) .fillo-scale-step:focus-visible {
448
+ border-color: var(--fillo-error);
449
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
450
+ outline: none;
451
+ }
419
452
  .fillo-scale-step--active {
420
453
  background: var(--fillo-primary);
421
454
  border-color: var(--fillo-primary);
@@ -480,6 +513,12 @@
480
513
  .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.8rem; }
481
514
  .fillo-matrix tbody th { text-align: start; font-weight: 500; }
482
515
  .fillo-matrix tbody tr:last-child th, .fillo-matrix tbody tr:last-child td { border-bottom: none; }
516
+ .fillo-matrix-cell:focus-within {
517
+ border-radius: var(--fillo-radius);
518
+ outline: 2px solid var(--fillo-primary);
519
+ outline-offset: 2px;
520
+ }
521
+ :where(.fillo-field--error) .fillo-matrix-cell:focus-within { outline-color: var(--fillo-error); }
483
522
 
484
523
  /* ---------- Calculated ---------- */
485
524
  /* A quiet computed-value line, not a control: no border or input chrome —
@@ -571,6 +610,15 @@
571
610
  border-color: var(--fillo-primary);
572
611
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
573
612
  }
613
+ :where(.fillo-field--error) .fillo-dropzone {
614
+ border-color: var(--fillo-error);
615
+ background: color-mix(in srgb, var(--fillo-error) 2%, var(--fillo-control-bg));
616
+ }
617
+ :where(.fillo-field--error) .fillo-dropzone:focus-visible {
618
+ border-color: var(--fillo-error);
619
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
620
+ outline: none;
621
+ }
574
622
  .fillo-dropzone--disabled { cursor: not-allowed; opacity: 0.6; }
575
623
  .fillo-dropzone-title { font-weight: 550; font-size: 0.95rem; }
576
624
  .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82rem; }
@@ -578,20 +626,50 @@
578
626
  .fillo-files { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; margin: 0; padding: 0; }
579
627
  .fillo-file {
580
628
  display: grid;
581
- grid-template-columns: minmax(0, 1fr) auto auto;
629
+ grid-template-columns: auto minmax(0, 1fr) auto;
582
630
  align-items: center;
583
- gap: 0.45rem 0.65rem;
631
+ gap: 0.45rem 0.625rem;
584
632
  border: 1px solid var(--fillo-border);
585
633
  border-radius: var(--fillo-radius);
586
634
  padding: 0.625rem 0.75rem;
587
635
  font-size: 0.88rem;
588
636
  background: var(--fillo-control-bg);
637
+ box-sizing: border-box;
638
+ min-width: 0;
589
639
  }
590
640
  .fillo-file--failed {
591
- grid-template-columns: minmax(0, 1fr) auto;
592
- border-color: var(--fillo-error);
641
+ border-color: color-mix(in srgb, var(--fillo-error) 32%, var(--fillo-border));
642
+ border-inline-start: 3px solid var(--fillo-error);
643
+ background: color-mix(in srgb, var(--fillo-error) 4%, var(--fillo-control-bg));
644
+ }
645
+ .fillo-file-state {
646
+ display: inline-flex;
647
+ align-items: center;
648
+ justify-content: center;
649
+ width: 1.5rem;
650
+ height: 1.5rem;
651
+ color: var(--fillo-muted);
652
+ }
653
+ .fillo-file-state--done { color: var(--fillo-primary); }
654
+ .fillo-file-state--failed { color: var(--fillo-error); }
655
+ .fillo-file-state-icon {
656
+ display: block;
657
+ width: 1.35rem;
658
+ height: 1.35rem;
659
+ stroke: currentColor;
660
+ stroke-width: 1.75;
661
+ stroke-linecap: round;
662
+ stroke-linejoin: round;
663
+ }
664
+ .fillo-file-content {
665
+ display: flex;
666
+ flex-direction: column;
667
+ gap: 0.1rem;
668
+ min-width: 0;
669
+ text-align: start;
593
670
  }
594
671
  .fillo-file-name {
672
+ display: block;
595
673
  min-width: 0;
596
674
  overflow: hidden;
597
675
  text-overflow: ellipsis;
@@ -609,14 +687,14 @@
609
687
  display: inline-flex;
610
688
  align-items: center;
611
689
  justify-content: flex-end;
612
- gap: 0.15rem;
690
+ gap: 0.25rem;
613
691
  white-space: nowrap;
614
692
  }
615
693
  .fillo-file-error {
616
- grid-column: 1 / -1;
617
694
  color: var(--fillo-error);
618
695
  font-size: 0.82rem;
619
696
  line-height: 1.35;
697
+ overflow-wrap: anywhere;
620
698
  }
621
699
  .fillo-file-remove {
622
700
  appearance: none;
@@ -627,11 +705,18 @@
627
705
  border: none;
628
706
  cursor: pointer;
629
707
  color: var(--fillo-muted);
630
- font-size: 1.1rem;
631
708
  line-height: 1;
632
709
  padding: 0;
633
710
  }
634
- .fillo-file-remove:hover { color: var(--fillo-error); }
711
+ .fillo-file-action-icon {
712
+ display: block;
713
+ width: 1rem;
714
+ height: 1rem;
715
+ stroke: currentColor;
716
+ stroke-width: 1.75;
717
+ stroke-linecap: round;
718
+ }
719
+ .fillo-file-remove:hover { color: var(--fillo-text); }
635
720
  .fillo-file-retry {
636
721
  appearance: none;
637
722
  display: inline-flex;
@@ -643,11 +728,11 @@
643
728
  color: var(--fillo-primary);
644
729
  font: inherit;
645
730
  font-weight: 550;
646
- padding: 0 0.3rem;
731
+ padding: 0 0.5rem;
647
732
  }
648
733
  .fillo-file-retry:hover { text-decoration: underline; }
649
734
  .fillo-progress {
650
- grid-column: 1 / -1;
735
+ grid-column: 2 / -1;
651
736
  height: 4px;
652
737
  border-radius: 999px;
653
738
  background: var(--fillo-border);
@@ -913,18 +998,6 @@
913
998
  line-height: 1.45;
914
999
  }
915
1000
 
916
- /* Error summary (GOV.UK pattern): ONE alert on failed submit; links focus
917
- their fields. Per-field messages are plain describedby text. */
918
- .fillo-error-summary {
919
- border: 2px solid var(--fillo-error);
920
- border-radius: var(--fillo-radius);
921
- padding: 0.8rem 1rem;
922
- }
923
- .fillo-error-summary-title { margin: 0 0 0.4rem; font-size: 1rem; font-weight: 600; }
924
- .fillo-error-summary ul { margin: 0; padding-inline-start: 1.1rem; }
925
- .fillo-error-summary li { margin: 0.2rem 0; }
926
- .fillo-error-summary a { color: var(--fillo-error); }
927
-
928
1001
  /* ---------- Accessibility: focus rings, pointer targets, forced-colors
929
1002
  (input-quality audit P1.1-P1.3) ---------- */
930
1003
  /* Focus visibility: controls without a compensating border swap get a
@@ -941,11 +1014,10 @@
941
1014
  outline-offset: 2px;
942
1015
  box-shadow: none;
943
1016
  }
1017
+ :where(.fillo-field--error) .fillo-star:focus-visible { outline-color: var(--fillo-error); }
944
1018
 
945
1019
  /* Minimum pointer targets (WCAG 2.2 AA 24px; 1.5rem = 24px @16px root). */
946
1020
  .fillo-ranking-move,
947
- .fillo-file-remove,
948
- .fillo-file-retry,
949
1021
  .fillo-signature-clear {
950
1022
  min-width: 1.5rem;
951
1023
  min-height: 1.5rem;
@@ -953,6 +1025,12 @@
953
1025
  align-items: center;
954
1026
  justify-content: center;
955
1027
  }
1028
+ /* Upload row actions use a calmer, easier 32px target. */
1029
+ .fillo-file-remove,
1030
+ .fillo-file-retry {
1031
+ min-width: 2rem;
1032
+ min-height: 2rem;
1033
+ }
956
1034
  .fillo-star { min-width: 1.75rem; min-height: 1.75rem; }
957
1035
  .fillo-matrix-cell {
958
1036
  display: flex;
@@ -974,6 +1052,7 @@
974
1052
  /* Matrix on narrow viewports: stacked rows, never a horizontal scroll
975
1053
  trap. Column names come from data-label (set by both renderers). */
976
1054
  @media (max-width: 30rem) {
1055
+ .fillo-file { gap: 0.4rem 0.5rem; padding: 0.55rem 0.625rem; }
977
1056
  .fillo-matrix thead { position: absolute; width: 1px; height: 1px;
978
1057
  overflow: hidden; clip-path: inset(50%); }
979
1058
  .fillo-matrix, .fillo-matrix tbody, .fillo-matrix tr,
@@ -991,6 +1070,12 @@
991
1070
 
992
1071
  /* Forced-colors: restore what author colors can't carry. */
993
1072
  @media (forced-colors: active) {
1073
+ .fillo-file--failed {
1074
+ border-color: CanvasText;
1075
+ border-inline-start-color: Highlight;
1076
+ background: Canvas;
1077
+ }
1078
+ .fillo-file-state--failed { color: Highlight; }
994
1079
  .fillo-star--active,
995
1080
  .fillo-scale-step--active,
996
1081
  :where(.fillo-option--selected) {
@@ -1001,7 +1086,11 @@
1001
1086
  .fillo-number:focus-within,
1002
1087
  .fillo-phone-flag:focus-visible,
1003
1088
  .fillo-phone-search:focus-visible,
1089
+ .fillo-option:focus-within,
1090
+ :where(.fillo-toggle-input:focus-visible) + .fillo-toggle-track,
1091
+ .fillo-star:focus-visible,
1004
1092
  .fillo-scale-step:focus-visible,
1093
+ .fillo-matrix-cell:focus-within,
1005
1094
  .fillo-dropzone:focus-visible,
1006
1095
  .fillo-select:focus-visible {
1007
1096
  outline: 2px solid Highlight;
@@ -84,11 +84,12 @@
84
84
  /* ---------- Field chrome ---------- */
85
85
  .fillo-field { display: flex; flex-direction: column; gap: 0.45rem; }
86
86
  .fillo-label { font-weight: 550; font-size: 0.95rem; }
87
+ :where(.fillo-field--error) .fillo-label { color: var(--fillo-error); }
87
88
  /* Fillo marks the few optional fields rather than starring the many required
88
89
  ones — so a clean label reads as required by default. */
89
90
  .fillo-optional { color: var(--fillo-muted); font-weight: 400; font-size: 0.85em; }
90
91
  .fillo-description { color: var(--fillo-muted); font-size: 0.85rem; margin: 0; line-height: 1.5; }
91
- .fillo-error { color: var(--fillo-error); font-size: 0.85rem; margin: 0; }
92
+ .fillo-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; line-height: 1.4; margin: 0; }
92
93
  .fillo-submit-error { color: var(--fillo-error); font-size: 0.85rem; font-weight: 500; margin: 0; }
93
94
 
94
95
  /* ---------- Inputs ---------- */
@@ -111,6 +112,11 @@
111
112
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
112
113
  }
113
114
  :where(.fillo-field--error) .fillo-input { border-color: var(--fillo-error); }
115
+ :where(.fillo-field--error) .fillo-input:focus-visible {
116
+ border-color: var(--fillo-error);
117
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
118
+ outline: none;
119
+ }
114
120
  .fillo-textarea { resize: vertical; min-height: 6rem; }
115
121
 
116
122
  /* Number field adornments: the wrapper draws the field's box (border/
@@ -134,6 +140,11 @@
134
140
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
135
141
  }
136
142
  :where(.fillo-field--error) .fillo-number { border-color: var(--fillo-error); }
143
+ :where(.fillo-field--error) .fillo-number:focus-within {
144
+ border-color: var(--fillo-error);
145
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
146
+ outline: none;
147
+ }
137
148
  :where(.fillo-number) .fillo-input {
138
149
  flex: 1 1 auto;
139
150
  min-width: 0;
@@ -143,6 +154,7 @@
143
154
  padding: 0;
144
155
  }
145
156
  :where(.fillo-number) .fillo-input:focus-visible { box-shadow: none; }
157
+ :where(.fillo-field--error) :where(.fillo-number) .fillo-input:focus-visible { outline: none; }
146
158
  .fillo-number-prefix, .fillo-number-suffix { flex: 0 0 auto; color: var(--fillo-muted); }
147
159
 
148
160
  /* Phone field: country picker + national input */
@@ -242,6 +254,15 @@
242
254
  border-color: var(--fillo-primary);
243
255
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 14%, transparent);
244
256
  }
257
+ :where(.fillo-field--error) .fillo-option {
258
+ border-color: var(--fillo-error);
259
+ background: color-mix(in srgb, var(--fillo-error) 2%, var(--fillo-control-bg));
260
+ }
261
+ :where(.fillo-field--error) .fillo-option:focus-within {
262
+ border-color: var(--fillo-error);
263
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
264
+ outline: none;
265
+ }
245
266
  .fillo-option--selected {
246
267
  border-color: var(--fillo-primary);
247
268
  background: color-mix(in srgb, var(--fillo-primary) 6%, var(--fillo-control-bg));
@@ -357,6 +378,13 @@
357
378
  inset 0 0 0 1px var(--fillo-primary),
358
379
  0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
359
380
  }
381
+ :where(.fillo-field--error) .fillo-toggle { border-color: var(--fillo-error); }
382
+ :where(.fillo-field--error) :where(.fillo-toggle-input:focus-visible) + .fillo-toggle-track {
383
+ box-shadow:
384
+ inset 0 0 0 1px var(--fillo-error),
385
+ 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
386
+ outline: none;
387
+ }
360
388
  .fillo-toggle:where(:has(.fillo-toggle-input:checked)) {
361
389
  border-color: var(--fillo-primary);
362
390
  background: color-mix(in srgb, var(--fillo-primary) 5%, var(--fillo-control-bg));
@@ -416,6 +444,11 @@
416
444
  border-color: var(--fillo-primary);
417
445
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
418
446
  }
447
+ :where(.fillo-field--error) .fillo-scale-step:focus-visible {
448
+ border-color: var(--fillo-error);
449
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
450
+ outline: none;
451
+ }
419
452
  .fillo-scale-step--active {
420
453
  background: var(--fillo-primary);
421
454
  border-color: var(--fillo-primary);
@@ -480,6 +513,12 @@
480
513
  .fillo-matrix thead th { color: var(--fillo-muted); font-weight: 500; font-size: 0.8rem; }
481
514
  .fillo-matrix tbody th { text-align: start; font-weight: 500; }
482
515
  .fillo-matrix tbody tr:last-child th, .fillo-matrix tbody tr:last-child td { border-bottom: none; }
516
+ .fillo-matrix-cell:focus-within {
517
+ border-radius: var(--fillo-radius);
518
+ outline: 2px solid var(--fillo-primary);
519
+ outline-offset: 2px;
520
+ }
521
+ :where(.fillo-field--error) .fillo-matrix-cell:focus-within { outline-color: var(--fillo-error); }
483
522
 
484
523
  /* ---------- Calculated ---------- */
485
524
  /* A quiet computed-value line, not a control: no border or input chrome —
@@ -571,6 +610,15 @@
571
610
  border-color: var(--fillo-primary);
572
611
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-primary) 15%, transparent);
573
612
  }
613
+ :where(.fillo-field--error) .fillo-dropzone {
614
+ border-color: var(--fillo-error);
615
+ background: color-mix(in srgb, var(--fillo-error) 2%, var(--fillo-control-bg));
616
+ }
617
+ :where(.fillo-field--error) .fillo-dropzone:focus-visible {
618
+ border-color: var(--fillo-error);
619
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--fillo-error) 20%, transparent);
620
+ outline: none;
621
+ }
574
622
  .fillo-dropzone--disabled { cursor: not-allowed; opacity: 0.6; }
575
623
  .fillo-dropzone-title { font-weight: 550; font-size: 0.95rem; }
576
624
  .fillo-dropzone-hint { color: var(--fillo-muted); font-size: 0.82rem; }
@@ -578,20 +626,50 @@
578
626
  .fillo-files { list-style: none; display: flex; flex-direction: column; gap: 0.45rem; margin: 0; padding: 0; }
579
627
  .fillo-file {
580
628
  display: grid;
581
- grid-template-columns: minmax(0, 1fr) auto auto;
629
+ grid-template-columns: auto minmax(0, 1fr) auto;
582
630
  align-items: center;
583
- gap: 0.45rem 0.65rem;
631
+ gap: 0.45rem 0.625rem;
584
632
  border: 1px solid var(--fillo-border);
585
633
  border-radius: var(--fillo-radius);
586
634
  padding: 0.625rem 0.75rem;
587
635
  font-size: 0.88rem;
588
636
  background: var(--fillo-control-bg);
637
+ box-sizing: border-box;
638
+ min-width: 0;
589
639
  }
590
640
  .fillo-file--failed {
591
- grid-template-columns: minmax(0, 1fr) auto;
592
- border-color: var(--fillo-error);
641
+ border-color: color-mix(in srgb, var(--fillo-error) 32%, var(--fillo-border));
642
+ border-inline-start: 3px solid var(--fillo-error);
643
+ background: color-mix(in srgb, var(--fillo-error) 4%, var(--fillo-control-bg));
644
+ }
645
+ .fillo-file-state {
646
+ display: inline-flex;
647
+ align-items: center;
648
+ justify-content: center;
649
+ width: 1.5rem;
650
+ height: 1.5rem;
651
+ color: var(--fillo-muted);
652
+ }
653
+ .fillo-file-state--done { color: var(--fillo-primary); }
654
+ .fillo-file-state--failed { color: var(--fillo-error); }
655
+ .fillo-file-state-icon {
656
+ display: block;
657
+ width: 1.35rem;
658
+ height: 1.35rem;
659
+ stroke: currentColor;
660
+ stroke-width: 1.75;
661
+ stroke-linecap: round;
662
+ stroke-linejoin: round;
663
+ }
664
+ .fillo-file-content {
665
+ display: flex;
666
+ flex-direction: column;
667
+ gap: 0.1rem;
668
+ min-width: 0;
669
+ text-align: start;
593
670
  }
594
671
  .fillo-file-name {
672
+ display: block;
595
673
  min-width: 0;
596
674
  overflow: hidden;
597
675
  text-overflow: ellipsis;
@@ -609,14 +687,14 @@
609
687
  display: inline-flex;
610
688
  align-items: center;
611
689
  justify-content: flex-end;
612
- gap: 0.15rem;
690
+ gap: 0.25rem;
613
691
  white-space: nowrap;
614
692
  }
615
693
  .fillo-file-error {
616
- grid-column: 1 / -1;
617
694
  color: var(--fillo-error);
618
695
  font-size: 0.82rem;
619
696
  line-height: 1.35;
697
+ overflow-wrap: anywhere;
620
698
  }
621
699
  .fillo-file-remove {
622
700
  appearance: none;
@@ -627,11 +705,18 @@
627
705
  border: none;
628
706
  cursor: pointer;
629
707
  color: var(--fillo-muted);
630
- font-size: 1.1rem;
631
708
  line-height: 1;
632
709
  padding: 0;
633
710
  }
634
- .fillo-file-remove:hover { color: var(--fillo-error); }
711
+ .fillo-file-action-icon {
712
+ display: block;
713
+ width: 1rem;
714
+ height: 1rem;
715
+ stroke: currentColor;
716
+ stroke-width: 1.75;
717
+ stroke-linecap: round;
718
+ }
719
+ .fillo-file-remove:hover { color: var(--fillo-text); }
635
720
  .fillo-file-retry {
636
721
  appearance: none;
637
722
  display: inline-flex;
@@ -643,11 +728,11 @@
643
728
  color: var(--fillo-primary);
644
729
  font: inherit;
645
730
  font-weight: 550;
646
- padding: 0 0.3rem;
731
+ padding: 0 0.5rem;
647
732
  }
648
733
  .fillo-file-retry:hover { text-decoration: underline; }
649
734
  .fillo-progress {
650
- grid-column: 1 / -1;
735
+ grid-column: 2 / -1;
651
736
  height: 4px;
652
737
  border-radius: 999px;
653
738
  background: var(--fillo-border);
@@ -913,18 +998,6 @@
913
998
  line-height: 1.45;
914
999
  }
915
1000
 
916
- /* Error summary (GOV.UK pattern): ONE alert on failed submit; links focus
917
- their fields. Per-field messages are plain describedby text. */
918
- .fillo-error-summary {
919
- border: 2px solid var(--fillo-error);
920
- border-radius: var(--fillo-radius);
921
- padding: 0.8rem 1rem;
922
- }
923
- .fillo-error-summary-title { margin: 0 0 0.4rem; font-size: 1rem; font-weight: 600; }
924
- .fillo-error-summary ul { margin: 0; padding-inline-start: 1.1rem; }
925
- .fillo-error-summary li { margin: 0.2rem 0; }
926
- .fillo-error-summary a { color: var(--fillo-error); }
927
-
928
1001
  /* ---------- Accessibility: focus rings, pointer targets, forced-colors
929
1002
  (input-quality audit P1.1-P1.3) ---------- */
930
1003
  /* Focus visibility: controls without a compensating border swap get a
@@ -941,11 +1014,10 @@
941
1014
  outline-offset: 2px;
942
1015
  box-shadow: none;
943
1016
  }
1017
+ :where(.fillo-field--error) .fillo-star:focus-visible { outline-color: var(--fillo-error); }
944
1018
 
945
1019
  /* Minimum pointer targets (WCAG 2.2 AA 24px; 1.5rem = 24px @16px root). */
946
1020
  .fillo-ranking-move,
947
- .fillo-file-remove,
948
- .fillo-file-retry,
949
1021
  .fillo-signature-clear {
950
1022
  min-width: 1.5rem;
951
1023
  min-height: 1.5rem;
@@ -953,6 +1025,12 @@
953
1025
  align-items: center;
954
1026
  justify-content: center;
955
1027
  }
1028
+ /* Upload row actions use a calmer, easier 32px target. */
1029
+ .fillo-file-remove,
1030
+ .fillo-file-retry {
1031
+ min-width: 2rem;
1032
+ min-height: 2rem;
1033
+ }
956
1034
  .fillo-star { min-width: 1.75rem; min-height: 1.75rem; }
957
1035
  .fillo-matrix-cell {
958
1036
  display: flex;
@@ -974,6 +1052,7 @@
974
1052
  /* Matrix on narrow viewports: stacked rows, never a horizontal scroll
975
1053
  trap. Column names come from data-label (set by both renderers). */
976
1054
  @media (max-width: 30rem) {
1055
+ .fillo-file { gap: 0.4rem 0.5rem; padding: 0.55rem 0.625rem; }
977
1056
  .fillo-matrix thead { position: absolute; width: 1px; height: 1px;
978
1057
  overflow: hidden; clip-path: inset(50%); }
979
1058
  .fillo-matrix, .fillo-matrix tbody, .fillo-matrix tr,
@@ -991,6 +1070,12 @@
991
1070
 
992
1071
  /* Forced-colors: restore what author colors can't carry. */
993
1072
  @media (forced-colors: active) {
1073
+ .fillo-file--failed {
1074
+ border-color: CanvasText;
1075
+ border-inline-start-color: Highlight;
1076
+ background: Canvas;
1077
+ }
1078
+ .fillo-file-state--failed { color: Highlight; }
994
1079
  .fillo-star--active,
995
1080
  .fillo-scale-step--active,
996
1081
  :where(.fillo-option--selected) {
@@ -1001,7 +1086,11 @@
1001
1086
  .fillo-number:focus-within,
1002
1087
  .fillo-phone-flag:focus-visible,
1003
1088
  .fillo-phone-search:focus-visible,
1089
+ .fillo-option:focus-within,
1090
+ :where(.fillo-toggle-input:focus-visible) + .fillo-toggle-track,
1091
+ .fillo-star:focus-visible,
1004
1092
  .fillo-scale-step:focus-visible,
1093
+ .fillo-matrix-cell:focus-within,
1005
1094
  .fillo-dropzone:focus-visible,
1006
1095
  .fillo-select:focus-visible {
1007
1096
  outline: 2px solid Highlight;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/react",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
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.12.0"
37
+ "@usefillo/core": "^0.12.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^18.0.0 || ^19.0.0",