@usefillo/core 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +36 -15
- package/dist/index.js +47 -15
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -66,9 +66,12 @@ interface NumberField extends BaseField {
|
|
|
66
66
|
decimals?: number;
|
|
67
67
|
prefix?: string;
|
|
68
68
|
suffix?: string;
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
|
|
69
|
+
/** Thousand separators in the SDK input. Display-only; never reaches the
|
|
70
|
+
* wire, the server, or exports. Named by the GROUP separator:
|
|
71
|
+
* - `"grouped"`: detect from the respondent's browser locale;
|
|
72
|
+
* - `"grouped-comma"`: fixed comma groups, dot decimal — `1,234.56`;
|
|
73
|
+
* - `"grouped-dot"`: fixed dot groups, comma decimal — `1.234,56`. */
|
|
74
|
+
notation?: "grouped" | "grouped-comma" | "grouped-dot";
|
|
72
75
|
}
|
|
73
76
|
interface ChoiceField extends BaseField {
|
|
74
77
|
kind: "select" | "multi_select" | "dropdown";
|
|
@@ -660,14 +663,26 @@ declare function normalizeFormTheme(input: unknown): FormTheme | null;
|
|
|
660
663
|
declare function formatAnswer(field: Field, value: FieldValue): string;
|
|
661
664
|
|
|
662
665
|
/**
|
|
663
|
-
* Grouped-number display + parsing for the Number field's `notation
|
|
664
|
-
* "grouped"`
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
666
|
+
* Grouped-number display + parsing for the Number field's `notation` input
|
|
667
|
+
* — browser-locale detection (`"grouped"`) or an author-fixed style
|
|
668
|
+
* (`"grouped-comma"` = `1,234.56`, `"grouped-dot"` = `1.234,56`); design
|
|
669
|
+
* contract: docs/decisions/number-formatting.md. All helpers are pure,
|
|
670
|
+
* side-effect-free, and DOM-free, so the same code runs in @usefillo/react,
|
|
671
|
+
* @usefillo/dom, and Node. All are strictly display-only — grouping never
|
|
672
|
+
* touches validation, canonical storage, or the wire (contract decision 2)
|
|
673
|
+
* — and all degrade to canonical text instead of throwing when `Intl` is
|
|
674
|
+
* unavailable.
|
|
670
675
|
*/
|
|
676
|
+
/**
|
|
677
|
+
* The `Intl` locale a `notation` value pins the separators to — the single
|
|
678
|
+
* source of the notation→locale map, consumed by BOTH renderers (never
|
|
679
|
+
* duplicate it). Notation values are named by the GROUP separator:
|
|
680
|
+
* - `"grouped"` (and unset): `undefined` — detect from the respondent's
|
|
681
|
+
* browser locale, exactly the pre-fixed-style behavior;
|
|
682
|
+
* - `"grouped-comma"`: `"en-US"` — comma groups, dot decimal, `1,234.56`;
|
|
683
|
+
* - `"grouped-dot"`: `"de-DE"` — dot groups, comma decimal, `1.234,56`.
|
|
684
|
+
*/
|
|
685
|
+
declare function localeForNotation(notation?: "grouped" | "grouped-comma" | "grouped-dot"): string | undefined;
|
|
671
686
|
/**
|
|
672
687
|
* Locale-grouped display text for a finite number — the blurred value of a
|
|
673
688
|
* `notation: "grouped"` number input. `maximumFractionDigits` is set
|
|
@@ -697,8 +712,10 @@ declare function formatGroupedNumber(value: number, opts?: {
|
|
|
697
712
|
* accepted as a group separator whenever the locale's own is whitespace;
|
|
698
713
|
* - maps the locale decimal mark to ".";
|
|
699
714
|
* - ALWAYS also accepts "." as a decimal mark regardless of locale — see
|
|
700
|
-
* {@link
|
|
701
|
-
*
|
|
715
|
+
* {@link resolveAmbiguousGrouping} for the locale families where a "." or
|
|
716
|
+
* "," group separator collides with a decimal reading (a "," group run
|
|
717
|
+
* that isn't exact 3-digit chunking reads as a decimal, so "12,5" under a
|
|
718
|
+
* comma-grouping locale is 12.5, never a silently-stripped 125);
|
|
702
719
|
* - preserves a leading minus;
|
|
703
720
|
* - preserves partial typing states ("1234." stays "1234." — `Number()` can
|
|
704
721
|
* still parse it, and rewriting mid-keystroke fights the respondent).
|
|
@@ -1362,7 +1379,9 @@ interface FormControllerOptions {
|
|
|
1362
1379
|
getHoneypot?: () => string;
|
|
1363
1380
|
/**
|
|
1364
1381
|
* @internal Let preview page navigation move forward without validating the
|
|
1365
|
-
* current page. Submission still validates
|
|
1382
|
+
* current page. Submission still validates every answerable field; a
|
|
1383
|
+
* transportless preview ignores required file-upload fields because its
|
|
1384
|
+
* disabled picker cannot possibly satisfy them. Renderers must not use this
|
|
1366
1385
|
* option to change visitor-facing behavior such as branding.
|
|
1367
1386
|
*/
|
|
1368
1387
|
skipValidation?: boolean;
|
|
@@ -1753,8 +1772,10 @@ interface FilloFieldStrings {
|
|
|
1753
1772
|
alreadyAnswered: string;
|
|
1754
1773
|
/** Retry control on a failed upload row. */
|
|
1755
1774
|
uploadRetry: string;
|
|
1756
|
-
/** Dropzone copy when uploads can't run
|
|
1775
|
+
/** Dropzone copy when uploads can't run because the form isn't connected. */
|
|
1757
1776
|
uploadsDisabled: string;
|
|
1777
|
+
/** Dropzone copy for an explicit render-only preview, which never has transport. */
|
|
1778
|
+
uploadsRenderOnly: string;
|
|
1758
1779
|
/** Dropzone copy when the server temporarily refuses new file sessions. */
|
|
1759
1780
|
uploadsUnavailable: string;
|
|
1760
1781
|
/** An upload request reached the server, but storage could not accept it. */
|
|
@@ -2037,4 +2058,4 @@ declare class Sha1 {
|
|
|
2037
2058
|
}
|
|
2038
2059
|
declare const sha1Base64: (bytes: Uint8Array) => string;
|
|
2039
2060
|
|
|
2040
|
-
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_CALC_MIN_SDK_VERSION, FILLO_CHALLENGE_MIN_SDK_VERSION, FILLO_DATA_ATTRS, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type JumpRule, type LinearScaleField, type MatrixField, type NextPage, type NumberField, PHONE_COUNTRIES, PHONE_PICKER_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsePhoneOptions, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type TrustPolicy, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, computeCalculated, conditionsMet, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, evaluateCalc, flagEmoji, formSchemasEqual, formatAnswer, formatGroupedNumber, formatNational, isAutoSubmitBlock, isBlockVisible, isBuildTimeDevEnv, isCodeForm, isField, isFilloError, isLikelyDevEnv, isPossiblePhone, isTerminalPage, isValidPartialNumberText, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parseGroupedNumber, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, radioGroupStep, reachableFieldIds, reachableFields, reachablePageIds, reachablePageSequence, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visiblePageBlocks, when };
|
|
2061
|
+
export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_CALC_MIN_SDK_VERSION, FILLO_CHALLENGE_MIN_SDK_VERSION, FILLO_DATA_ATTRS, FILLO_MIN_SDK_VERSION, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, FILLO_THEME_VARS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, type FilloFieldStrings, FilloJsxError, type FilloRendererStrings, type FilloRespondent, type FilloSlot, type FilloStrings, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, JSX_BLOCK_COMPONENTS, JSX_BLOCK_SPECS, type JsonValue, type JsxFormMeta, type JumpRule, type LinearScaleField, type MatrixField, type NextPage, type NumberField, PHONE_COUNTRIES, PHONE_PICKER_COUNTRIES, PHONE_POPOVER_VIEWPORT_GAP, type ParagraphBlock, type ParsePhoneOptions, type ParsedPhone, type PhoneCountry, type PhoneField, type PhonePopoverPlacement, type ProvisionWorkspaceResult, type PublishedForm, REQUIRED_FIELD_MESSAGE, type RankingField, type RatingField, type ResponseData, type ResponseDraft, type ResponseLimit, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type TrustPolicy, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, computeCalculated, conditionsMet, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, evaluateCalc, flagEmoji, formSchemasEqual, formatAnswer, formatGroupedNumber, formatNational, isAutoSubmitBlock, isBlockVisible, isBuildTimeDevEnv, isCodeForm, isField, isFilloError, isLikelyDevEnv, isPossiblePhone, isTerminalPage, isValidPartialNumberText, localeForNotation, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, normalizeSettings, parseGroupedNumber, parsePhone, pipeBlock, positionPhonePopover, prefillFromParams, provisionWorkspace, radioGroupStep, reachableFieldIds, reachableFields, reachablePageIds, reachablePageSequence, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visiblePageBlocks, when };
|
package/dist/index.js
CHANGED
|
@@ -666,10 +666,12 @@ function positionPhonePopover(anchor, popover) {
|
|
|
666
666
|
popover.style.setProperty("--fillo-phone-popover-max-width", `${Math.floor(maxWidth)}px`);
|
|
667
667
|
popover.style.setProperty("--fillo-phone-popover-offset-x", "0px");
|
|
668
668
|
const width = Math.min(popover.offsetWidth, maxWidth);
|
|
669
|
+
const rtl = getComputedStyle(anchor).direction === "rtl";
|
|
670
|
+
const unshiftedLeft = rtl ? anchorRect.right - width : anchorRect.left;
|
|
669
671
|
let offsetX = 0;
|
|
670
|
-
const rightOverflow =
|
|
672
|
+
const rightOverflow = unshiftedLeft + width - (viewportWidth - gap);
|
|
671
673
|
if (rightOverflow > 0) offsetX -= rightOverflow;
|
|
672
|
-
const leftOverflow = gap - (
|
|
674
|
+
const leftOverflow = gap - (unshiftedLeft + offsetX);
|
|
673
675
|
if (leftOverflow > 0) offsetX += leftOverflow;
|
|
674
676
|
popover.style.setProperty("--fillo-phone-popover-offset-x", `${Math.round(offsetX)}px`);
|
|
675
677
|
return placement;
|
|
@@ -710,7 +712,8 @@ var DEFAULT_FIELD_STRINGS = {
|
|
|
710
712
|
resumeLinkExpired: "That saved-progress link expired or was already used \u2014 start again below.",
|
|
711
713
|
alreadyAnswered: "You've already answered this form.",
|
|
712
714
|
uploadRetry: "Retry",
|
|
713
|
-
uploadsDisabled: "
|
|
715
|
+
uploadsDisabled: "Connect this form to Fillo to enable uploads",
|
|
716
|
+
uploadsRenderOnly: "Uploads are unavailable in this render-only preview",
|
|
714
717
|
uploadsUnavailable: "Uploads are temporarily unavailable",
|
|
715
718
|
uploadUnavailable: "We couldn't upload this file right now. Try again in a moment.",
|
|
716
719
|
uploadFailed: "Upload failed \u2014 try again",
|
|
@@ -959,7 +962,7 @@ var schemaShape = z2.object({
|
|
|
959
962
|
});
|
|
960
963
|
var MAX_SCHEMA_VERSION = 1;
|
|
961
964
|
var FILLO_SCHEMA_VERSION = 1;
|
|
962
|
-
var FILLO_SDK_VERSION = true ? "0.
|
|
965
|
+
var FILLO_SDK_VERSION = true ? "0.12.0" : "0.0.0-dev";
|
|
963
966
|
var FILLO_MIN_SDK_VERSION = "0.4.0";
|
|
964
967
|
var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
965
968
|
var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
|
|
@@ -1346,9 +1349,9 @@ function normalizeBlock(input) {
|
|
|
1346
1349
|
decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
|
|
1347
1350
|
prefix: optionalAffix(rec.prefix, 100),
|
|
1348
1351
|
suffix: optionalAffix(rec.suffix, 100),
|
|
1349
|
-
// The only supported
|
|
1352
|
+
// The only supported values; anything else drops silently rather than
|
|
1350
1353
|
// erroring (an old/garbage value is just "no grouping").
|
|
1351
|
-
notation: rec.notation === "grouped" ? "grouped" : void 0
|
|
1354
|
+
notation: rec.notation === "grouped" ? "grouped" : rec.notation === "grouped-comma" ? "grouped-comma" : rec.notation === "grouped-dot" ? "grouped-dot" : void 0
|
|
1352
1355
|
});
|
|
1353
1356
|
case "select":
|
|
1354
1357
|
case "multi_select":
|
|
@@ -1640,6 +1643,17 @@ function formatAnswer(field2, value) {
|
|
|
1640
1643
|
function clampDecimals2(decimals) {
|
|
1641
1644
|
return Math.max(0, Math.min(6, Math.round(decimals)));
|
|
1642
1645
|
}
|
|
1646
|
+
function localeForNotation(notation) {
|
|
1647
|
+
switch (notation) {
|
|
1648
|
+
case "grouped-comma":
|
|
1649
|
+
return "en-US";
|
|
1650
|
+
case "grouped-dot":
|
|
1651
|
+
return "de-DE";
|
|
1652
|
+
case "grouped":
|
|
1653
|
+
case void 0:
|
|
1654
|
+
return void 0;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1643
1657
|
function formatGroupedNumber(value, opts) {
|
|
1644
1658
|
if (!Number.isFinite(value)) return String(value);
|
|
1645
1659
|
const decimals = opts?.decimals === void 0 ? void 0 : clampDecimals2(opts.decimals);
|
|
@@ -1661,17 +1675,17 @@ function stripAll(text, separator) {
|
|
|
1661
1675
|
function toCanonicalDecimal(text, decimal) {
|
|
1662
1676
|
return decimal === "." ? text : text.split(decimal).join(".");
|
|
1663
1677
|
}
|
|
1664
|
-
function
|
|
1665
|
-
if (decimal !==
|
|
1678
|
+
function resolveAmbiguousGrouping(body, group, decimal) {
|
|
1679
|
+
if (decimal !== group && body.includes(decimal)) {
|
|
1666
1680
|
const i = body.lastIndexOf(decimal);
|
|
1667
|
-
const intPart = stripAll(body.slice(0, i),
|
|
1681
|
+
const intPart = stripAll(body.slice(0, i), group);
|
|
1668
1682
|
const fracPart = body.slice(i + decimal.length);
|
|
1669
1683
|
return `${intPart}.${fracPart}`;
|
|
1670
1684
|
}
|
|
1671
|
-
const segments = body.split(
|
|
1685
|
+
const segments = body.split(group);
|
|
1672
1686
|
if (segments.length === 1) return body;
|
|
1673
1687
|
const isPureGrouping = segments.every(
|
|
1674
|
-
(seg, i) => i === 0 ?
|
|
1688
|
+
(seg, i) => i === 0 ? /^[1-9]\d{0,2}$/.test(seg) : /^\d{3}$/.test(seg)
|
|
1675
1689
|
);
|
|
1676
1690
|
if (isPureGrouping) return segments.join("");
|
|
1677
1691
|
return `${segments.slice(0, -1).join("")}.${segments.at(-1) ?? ""}`;
|
|
@@ -1687,8 +1701,8 @@ function parseGroupedNumber(text, locale) {
|
|
|
1687
1701
|
if (negative) body = body.slice(1);
|
|
1688
1702
|
if (/^\s$/.test(group)) {
|
|
1689
1703
|
body = toCanonicalDecimal(body.replace(/\s/g, ""), decimal);
|
|
1690
|
-
} else if (group === ".") {
|
|
1691
|
-
body =
|
|
1704
|
+
} else if (group === "." || group === ",") {
|
|
1705
|
+
body = resolveAmbiguousGrouping(body, group, decimal);
|
|
1692
1706
|
} else {
|
|
1693
1707
|
body = toCanonicalDecimal(stripAll(body, group), decimal);
|
|
1694
1708
|
}
|
|
@@ -3366,9 +3380,26 @@ function createFormController(options2) {
|
|
|
3366
3380
|
if (status === "submitting" || status === "submitted") return;
|
|
3367
3381
|
const result = validateResponse(form, data);
|
|
3368
3382
|
if (!result.ok) {
|
|
3369
|
-
|
|
3383
|
+
const transportlessPreview = options2.skipValidation && !(client && formId);
|
|
3384
|
+
const previewErrors = transportlessPreview ? Object.fromEntries(
|
|
3385
|
+
Object.entries(result.errors).filter(
|
|
3386
|
+
([fieldId]) => form.pages.every(
|
|
3387
|
+
(page) => page.blocks.every(
|
|
3388
|
+
(block) => !(isField(block) && block.id === fieldId && block.kind === "file_upload")
|
|
3389
|
+
)
|
|
3390
|
+
)
|
|
3391
|
+
)
|
|
3392
|
+
) : result.errors;
|
|
3393
|
+
if (transportlessPreview && Object.keys(previewErrors).length === 0) {
|
|
3394
|
+
errors = {};
|
|
3395
|
+
status = "submitted";
|
|
3396
|
+
notify();
|
|
3397
|
+
emitSubmitted(void 0, data);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
errors = previewErrors;
|
|
3370
3401
|
const firstBad = form.pages.findIndex(
|
|
3371
|
-
(p) => visiblePageBlocks(form, p, data).some((b) => b.id in
|
|
3402
|
+
(p) => visiblePageBlocks(form, p, data).some((b) => b.id in previewErrors)
|
|
3372
3403
|
);
|
|
3373
3404
|
if (firstBad >= 0) pageIndex = firstBad;
|
|
3374
3405
|
notify();
|
|
@@ -4252,6 +4283,7 @@ export {
|
|
|
4252
4283
|
isPossiblePhone,
|
|
4253
4284
|
isTerminalPage,
|
|
4254
4285
|
isValidPartialNumberText,
|
|
4286
|
+
localeForNotation,
|
|
4255
4287
|
needsExplicitSubmit,
|
|
4256
4288
|
normalizeFormSchema,
|
|
4257
4289
|
normalizeFormTheme,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefillo/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Form schema, validation, logic engine and JS client for Fillo. Framework-agnostic.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"zod": "^4.4.3"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"jsdom": "^25.0.1",
|
|
35
36
|
"tsup": "^8.4.0",
|
|
36
37
|
"typescript": "^5.8.3"
|
|
37
38
|
},
|