@usefillo/core 0.12.1 → 0.14.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 CHANGED
@@ -20,7 +20,7 @@ interface JumpRule {
20
20
  /** Target page id, or "end" to finish the form. */
21
21
  to: string | "end";
22
22
  }
23
- type FieldKind = "short_text" | "long_text" | "email" | "url" | "phone" | "number" | "select" | "multi_select" | "dropdown" | "checkbox" | "rating" | "linear_scale" | "ranking" | "matrix" | "signature" | "date" | "file_upload" | "hidden" | "calculated" | "custom";
23
+ type FieldKind = "short_text" | "long_text" | "email" | "url" | "phone" | "number" | "select" | "multi_select" | "dropdown" | "checkbox" | "rating" | "linear_scale" | "ranking" | "matrix" | "signature" | "date" | "file_upload" | "hidden" | "calculated" | "repeating_group" | "custom";
24
24
  type ContentKind = "heading" | "paragraph" | "divider";
25
25
  type BlockKind = FieldKind | ContentKind;
26
26
  interface SelectOption {
@@ -198,6 +198,53 @@ interface CalculatedField extends BaseField {
198
198
  prefix?: string;
199
199
  suffix?: string;
200
200
  }
201
+ /**
202
+ * One repeated instance's answers: childId → that child's canonical value,
203
+ * the exact same shapes a top-level field would carry (trimmed strings,
204
+ * wire-format numerics coerced, empty answers absent — see canonical.ts). A
205
+ * key absent from the record means that child is unanswered in this instance,
206
+ * mirroring how a key absent from {@link ResponseData} means a top-level field
207
+ * is unanswered.
208
+ */
209
+ type GroupInstanceValue = Record<string, FieldValue>;
210
+ /**
211
+ * A repeated block of fields (logic depth P3) — "one row per guest/item",
212
+ * respondent-controlled. Bounded by construction: `maxInstances` is required
213
+ * (1–20); `minInstances` clamps to 0..maxInstances (default 1). The template
214
+ * (`fields`) is capped at 12 children, drawn from a restricted v1 allowlist
215
+ * (see schema-validation's GROUP_CHILD_ALLOWED_KINDS) — notably NOT
216
+ * `repeating_group` itself (nesting is out of scope for v1) and NOT
217
+ * `calculated` (no per-instance calc plumbing yet). Child ids are unique only
218
+ * WITHIN the group (the select-option precedent, not the global block
219
+ * namespace), so the same child id can recur across two different groups.
220
+ *
221
+ * Value shape (locked, roadmap decision 4):
222
+ * `data[groupId]: GroupInstanceValue[]`, instances in respondent order. Key
223
+ * absent ⇔ zero instances (unanswered iff `minInstances` is 0).
224
+ *
225
+ * Scope walls (v1, strict): a child's `visibleIf` may reference ONLY
226
+ * same-group sibling ids, evaluated per instance over that instance's
227
+ * canonical values (see logic.ts's `visibleGroupChildren`). Nothing outside
228
+ * the group — top-level visibility, jumps, calc operands, piping — can
229
+ * reference a child id; both directions are schema hard errors.
230
+ */
231
+ interface RepeatingGroupField extends BaseField {
232
+ kind: "repeating_group";
233
+ /** The repeated template. Child ids are unique WITHIN the group (the
234
+ * select-option precedent, not the global block namespace). */
235
+ fields: Field[];
236
+ /** Respondent-controlled instance count, clamped server-side. 0..maxInstances,
237
+ * default 1. */
238
+ minInstances?: number;
239
+ /** REQUIRED, 1..20 — bounded by design (also bounds the authoring-time
240
+ * worst-case payload-size estimate). */
241
+ maxInstances: number;
242
+ /** "Add another" default via strings. */
243
+ addLabel?: string;
244
+ /** Names one instance — "Guest" → headings "Guest 2 of 3", announcements,
245
+ * CSV column prefixes. Defaults to the field label. */
246
+ itemLabel?: string;
247
+ }
201
248
  /**
202
249
  * A field type Fillo doesn't ship. You define it in code and supply the
203
250
  * renderer via the SDK's `customComponents` map, keyed by `component`. The
@@ -212,7 +259,7 @@ interface CustomField extends BaseField {
212
259
  /** Arbitrary options handed to your component. */
213
260
  config?: Record<string, unknown>;
214
261
  }
215
- type Field = TextField | PhoneField | NumberField | ChoiceField | CheckboxField | RatingField | LinearScaleField | RankingField | MatrixField | SignatureField | DateField | FileUploadField | HiddenField | CalculatedField | CustomField;
262
+ type Field = TextField | PhoneField | NumberField | ChoiceField | CheckboxField | RatingField | LinearScaleField | RankingField | MatrixField | SignatureField | DateField | FileUploadField | HiddenField | CalculatedField | RepeatingGroupField | CustomField;
216
263
  interface HeadingBlock extends BaseBlock {
217
264
  kind: "heading";
218
265
  text: string;
@@ -389,6 +436,10 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
389
436
  type FieldValue = string | number | boolean | string[] | FileValue[]
390
437
  /** Matrix answers: rowId → columnId. */
391
438
  | Record<string, string>
439
+ /** Repeating-group answers: one record per instance, in respondent order
440
+ * (roadmap decision 4). A NAMED member — never smuggled through
441
+ * JsonValue — so the discriminated union stays exhaustiveness-checkable. */
442
+ | GroupInstanceValue[]
392
443
  /** Custom fields hold arbitrary JSON. */
393
444
  | JsonValue | null | undefined;
394
445
  type ResponseData = Record<string, FieldValue>;
@@ -491,6 +542,27 @@ declare function allFields(form: FormSchema): Field[];
491
542
  declare function responseScopeValue(settings: FormSettings, data: ResponseData): string | null;
492
543
  /** Fields currently visible given the response data. */
493
544
  declare function visibleFields(form: FormSchema, data: ResponseData): Field[];
545
+ /**
546
+ * THE scoped per-instance visibility resolver for a repeating group's
547
+ * children (contract decisions 3 + 4): given ONE instance's values, the
548
+ * template children currently visible in that instance, in template order.
549
+ * Both renderers (which rows to draw in instance n) and validateResponse
550
+ * (which children to require/keep in instance n) consume THIS one helper —
551
+ * no second engine. It literally IS the shared engine: the children run
552
+ * through the same {@link resolveLogicState} fixpoint a page of top-level
553
+ * fields would, over the instance's values — so canonical-value discipline
554
+ * (trim/coerce/empty-drop before any condition reads a sibling) and
555
+ * hidden-sibling cascading (a hidden controller reads as unanswered) behave
556
+ * exactly like top level, one scope down. Scope walls are enforced at schema
557
+ * time: a child's visibleIf can only name same-group siblings, so the
558
+ * instance's own values are the complete condition universe — outer answers
559
+ * can never matter here, and child values never leak out (the group is one
560
+ * opaque field in the top-level fixpoint). No calc pass runs because
561
+ * `calculated` children are schema-rejected (decision 2). A logic-hidden
562
+ * child is unanswered for its siblings AND skipped by per-instance
563
+ * validation, mirroring the top-level discipline.
564
+ */
565
+ declare function visibleGroupChildren(group: RepeatingGroupField, instanceValues: GroupInstanceValue): Field[];
494
566
  /** Where navigation goes when leaving a page. */
495
567
  type NextPage = {
496
568
  to: string;
@@ -637,6 +709,16 @@ declare const FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
637
709
  * applicable floors).
638
710
  */
639
711
  declare const FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
712
+ /**
713
+ * The floor served INSTEAD of FILLO_MIN_SDK_VERSION for forms whose live
714
+ * schema contains a repeating_group field: the first release that ships the
715
+ * kind. Stricter than the calc case (recon risk 5): an old SDK's zod enum
716
+ * doesn't just strip one row, it drops the WHOLE templated section silently —
717
+ * worse than a blank row, because there is no visual trace a question ever
718
+ * existed. Non-negotiable; the form GET picks the max of the applicable
719
+ * floors (the Turnstile/calc precedent).
720
+ */
721
+ declare const FILLO_GROUP_MIN_SDK_VERSION = "0.13.0";
640
722
  declare function normalizeSettings(value: unknown): FormSettings;
641
723
  interface SchemaValidationResult {
642
724
  ok: boolean;
@@ -899,6 +981,14 @@ declare function createEmptyForm(title?: string): FormSchema;
899
981
  * first two are code-only concerns and signature is too niche to draft well)
900
982
  * and calculated (an LLM can't reliably wire a calc AST to stable field ids —
901
983
  * owners add calculations deliberately, in the builder or in code).
984
+ * Also excludes repeating_group (contract decision 11): the flat FieldSpec →
985
+ * createBlock-defaults assembly has no nested-template channel, and a group
986
+ * needs structural choices an LLM gets wrong in ways the assembler can't
987
+ * repair — per-group-unique child ids, the v1 child-kind allowlist, and a
988
+ * deliberate maxInstances bound (required, 1–20). A malformed group is a
989
+ * schema HARD error, so a hallucinated one would brick the whole draft rather
990
+ * than degrade a field. Owners add groups deliberately, in the builder or in
991
+ * code — the calculated precedent.
902
992
  */
903
993
  declare const DRAFT_KINDS: readonly BlockKind[];
904
994
  /** The LLM-friendly intermediate shape. Everything optional but `kind`. */
@@ -1028,6 +1118,16 @@ interface SubmitMeta {
1028
1118
  elapsedMs?: number;
1029
1119
  /** Embedding surface, recorded per response for measurement — never gated. */
1030
1120
  surface?: "default" | "headless";
1121
+ /**
1122
+ * Browser page context used by connected analytics/CRM destinations. Fillo
1123
+ * reads an existing HubSpot tracking cookie when present but never creates
1124
+ * one or loads tracking code.
1125
+ */
1126
+ attribution?: {
1127
+ pageUri?: string;
1128
+ pageName?: string;
1129
+ hubspotutk?: string;
1130
+ };
1031
1131
  /**
1032
1132
  * Browser-scoped de-duplication key sent when a form opts into
1033
1133
  * settings.responseLimit.by = "browser".
@@ -1814,6 +1914,16 @@ interface FilloFieldStrings {
1814
1914
  errorSummaryTitle: string;
1815
1915
  /** Live-region announcement after a ranking move: "«label», position n of m". */
1816
1916
  rankingPosition: (label: string, position: number, count: number) => string;
1917
+ /** Repeating group: the Add button's default label. */
1918
+ groupAdd: string;
1919
+ /** Repeating group instance heading — "«item» n of m". */
1920
+ groupInstanceLabel: (item: string, position: number, count: number) => string;
1921
+ /** Repeating group per-instance Remove button accessible label. */
1922
+ groupRemoveLabel: (item: string, position: number) => string;
1923
+ /** Live-region announcement after adding an instance. */
1924
+ groupInstanceAdded: (item: string, position: number, count: number) => string;
1925
+ /** Live-region announcement after removing an instance. */
1926
+ groupInstanceRemoved: (item: string, count: number) => string;
1817
1927
  /** Live-region announcement once a phone country-picker selection commits
1818
1928
  * (focus moves straight to the national input, so nothing else announces it). */
1819
1929
  phoneCountrySelected: (name: string) => string;
@@ -1954,6 +2064,11 @@ interface BlockSpec {
1954
2064
  optionChildren?: boolean;
1955
2065
  /** String children become the `text` prop (heading/paragraph). */
1956
2066
  textChildren?: boolean;
2067
+ /** Accepts Fillo FIELD elements as children, compiled into the `fields`
2068
+ * template (repeating groups). The walk's flatten/classify/buildProps trio
2069
+ * is reentrant, so children build through the exact same path as top-level
2070
+ * blocks. Child ids are per-GROUP (checked here), not global. */
2071
+ blockChildren?: boolean;
1957
2072
  /** Content blocks carry no label/required/etc. */
1958
2073
  content?: boolean;
1959
2074
  }
@@ -2074,4 +2189,4 @@ declare class Sha1 {
2074
2189
  }
2075
2190
  declare const sha1Base64: (bytes: Uint8Array) => string;
2076
2191
 
2077
- 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, requiredFieldMessage, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visiblePageBlocks, when };
2192
+ 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_GROUP_MIN_SDK_VERSION, 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 GroupInstanceValue, 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 RepeatingGroupField, 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, requiredFieldMessage, resolveNextPage, resolveSlotClass, resolveStrings, resolveText, resolveThemeAppearance, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visibleGroupChildren, visiblePageBlocks, when };
package/dist/index.js CHANGED
@@ -13,9 +13,38 @@ function isEmpty(value) {
13
13
  if (typeof value === "object") return Object.keys(value).length === 0;
14
14
  return false;
15
15
  }
16
+ function normalizeGroupValue(field2, value) {
17
+ if (!Array.isArray(value)) return void 0;
18
+ const childById = new Map(field2.fields.map((child) => [child.id, child]));
19
+ let changed = false;
20
+ const instances = value.map((instance) => {
21
+ if (!instance || typeof instance !== "object" || Array.isArray(instance)) return instance;
22
+ let instanceChanged = false;
23
+ const out = {};
24
+ for (const [key, raw] of Object.entries(instance)) {
25
+ const child = childById.get(key);
26
+ if (!child) {
27
+ instanceChanged = true;
28
+ continue;
29
+ }
30
+ const canonical = canonicalValue(child, raw);
31
+ if (canonical === void 0) {
32
+ instanceChanged = true;
33
+ continue;
34
+ }
35
+ out[key] = canonical;
36
+ if (canonical !== raw) instanceChanged = true;
37
+ }
38
+ if (!instanceChanged) return instance;
39
+ changed = true;
40
+ return out;
41
+ });
42
+ return changed ? instances : value;
43
+ }
16
44
  function normalizeValue(field2, value) {
17
45
  if (value === void 0 || value === null) return value;
18
46
  if (field2.kind === "custom") return value;
47
+ if (field2.kind === "repeating_group") return normalizeGroupValue(field2, value);
19
48
  if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
20
49
  const n = Number(value);
21
50
  return Number.isNaN(n) ? value : n;
@@ -190,7 +219,12 @@ function resolveLogicState(fields, data) {
190
219
  for (const f of fields) {
191
220
  if (f.kind === "calculated") continue;
192
221
  const value = canonicalValue(f, data[f.id]);
193
- if (value !== void 0) canonical[f.id] = value;
222
+ if (value === void 0) continue;
223
+ if (f.kind === "repeating_group") {
224
+ const count = Array.isArray(value) ? value.length : 0;
225
+ if (count < Math.max(1, f.minInstances ?? 1)) continue;
226
+ }
227
+ canonical[f.id] = value;
194
228
  }
195
229
  let visible = scoped;
196
230
  let calc = {};
@@ -257,6 +291,11 @@ function visibleFields(form, data) {
257
291
  const visible = visibleFieldIds(fields, data);
258
292
  return fields.filter((f) => visible.has(f.id));
259
293
  }
294
+ function visibleGroupChildren(group, instanceValues) {
295
+ const values = instanceValues && typeof instanceValues === "object" && !Array.isArray(instanceValues) ? instanceValues : {};
296
+ const state = resolveLogicState(group.fields, values);
297
+ return group.fields.filter((f) => state.visible.has(f.id));
298
+ }
260
299
  function formResolver(form, data) {
261
300
  return resolveLogicState(allFields(form), data).resolve;
262
301
  }
@@ -754,6 +793,11 @@ var DEFAULT_FIELD_STRINGS = {
754
793
  submittingAnnouncement: "Submitting your response\u2026",
755
794
  errorSummaryTitle: "Check these fields",
756
795
  rankingPosition: (label, position, count) => `${label}, position ${position} of ${count}`,
796
+ groupAdd: "Add another",
797
+ groupInstanceLabel: (item, position, count) => `${item} ${position} of ${count}`,
798
+ groupRemoveLabel: (item, position) => `Remove ${item} ${position}`,
799
+ groupInstanceAdded: (item, position, count) => `${item} ${position} added, ${position} of ${count}`,
800
+ groupInstanceRemoved: (item, count) => count === 0 ? `${item} removed, none left` : `${item} removed, ${count} remaining`,
757
801
  phoneCountrySelected: (name) => `${name} selected`,
758
802
  phoneResultsCount: (count) => `${count} ${count === 1 ? "result" : "results"}`,
759
803
  signatureEmpty: "No signature yet",
@@ -778,9 +822,17 @@ var fileValueSchema = z.object({
778
822
  mime: z.string(),
779
823
  url: z.optional(z.string())
780
824
  });
825
+ function groupCountError(field2, count) {
826
+ const min = Math.max(0, field2.minInstances ?? 1);
827
+ if (count < min) return `Add at least ${min} ${min === 1 ? "entry" : "entries"}`;
828
+ if (Number.isFinite(field2.maxInstances) && count > field2.maxInstances)
829
+ return `At most ${field2.maxInstances} ${field2.maxInstances === 1 ? "entry" : "entries"}`;
830
+ return null;
831
+ }
781
832
  function validateField(field2, value) {
782
833
  if (isEmpty(value)) {
783
834
  if (field2.kind === "calculated") return null;
835
+ if (field2.kind === "repeating_group") return groupCountError(field2, 0);
784
836
  return field2.required ? REQUIRED_FIELD_MESSAGE : null;
785
837
  }
786
838
  switch (field2.kind) {
@@ -900,6 +952,17 @@ function validateField(field2, value) {
900
952
  case "calculated": {
901
953
  return null;
902
954
  }
955
+ case "repeating_group": {
956
+ if (!Array.isArray(value)) {
957
+ return groupCountError(field2, 0);
958
+ }
959
+ const instances = value;
960
+ for (const instance of instances) {
961
+ if (!instance || typeof instance !== "object" || Array.isArray(instance))
962
+ return "Invalid value";
963
+ }
964
+ return groupCountError(field2, instances.length);
965
+ }
903
966
  case "custom": {
904
967
  return null;
905
968
  }
@@ -929,6 +992,10 @@ function validateResponse(form, data) {
929
992
  for (const field2 of fields) {
930
993
  if (field2.kind === "calculated") continue;
931
994
  const value = normalizeValue(field2, data[field2.id]);
995
+ if (field2.kind === "repeating_group") {
996
+ validateGroupResponse(field2, value, errors, cleaned);
997
+ continue;
998
+ }
932
999
  const error = validateField(field2, value);
933
1000
  if (error) {
934
1001
  errors[field2.id] = error;
@@ -938,6 +1005,29 @@ function validateResponse(form, data) {
938
1005
  }
939
1006
  return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
940
1007
  }
1008
+ function validateGroupResponse(field2, value, errors, cleaned) {
1009
+ const groupError = validateField(field2, value);
1010
+ if (groupError) errors[field2.id] = groupError;
1011
+ if (!Array.isArray(value)) return;
1012
+ const instances = value;
1013
+ const kept = [];
1014
+ for (let index = 0; index < instances.length; index++) {
1015
+ const instance = instances[index];
1016
+ if (!instance || typeof instance !== "object" || Array.isArray(instance)) continue;
1017
+ const keptInstance = {};
1018
+ for (const child of visibleGroupChildren(field2, instance)) {
1019
+ const childValue = instance[child.id];
1020
+ const childError = validateField(child, childValue);
1021
+ if (childError) {
1022
+ errors[`${field2.id}.${index}.${child.id}`] = childError;
1023
+ } else if (!isEmpty(childValue)) {
1024
+ keptInstance[child.id] = childValue;
1025
+ }
1026
+ }
1027
+ kept.push(keptInstance);
1028
+ }
1029
+ if (!groupError && kept.length > 0) cleaned[field2.id] = kept;
1030
+ }
941
1031
 
942
1032
  // src/schema-validation.ts
943
1033
  import * as z2 from "zod/mini";
@@ -961,6 +1051,7 @@ var FIELD_KINDS = [
961
1051
  "file_upload",
962
1052
  "hidden",
963
1053
  "calculated",
1054
+ "repeating_group",
964
1055
  "custom"
965
1056
  ];
966
1057
  var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
@@ -997,10 +1088,11 @@ var schemaShape = z2.object({
997
1088
  });
998
1089
  var MAX_SCHEMA_VERSION = 1;
999
1090
  var FILLO_SCHEMA_VERSION = 1;
1000
- var FILLO_SDK_VERSION = true ? "0.12.1" : "0.0.0-dev";
1091
+ var FILLO_SDK_VERSION = true ? "0.14.0" : "0.0.0-dev";
1001
1092
  var FILLO_MIN_SDK_VERSION = "0.4.0";
1002
1093
  var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
1003
1094
  var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
1095
+ var FILLO_GROUP_MIN_SDK_VERSION = "0.13.0";
1004
1096
  function str(value, max, fallback = "") {
1005
1097
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
1006
1098
  }
@@ -1170,7 +1262,7 @@ function collectCalcRefs(expr, valueRefs, whenRefs) {
1170
1262
  return;
1171
1263
  }
1172
1264
  }
1173
- function validateCalculatedFields(pages) {
1265
+ function validateCalculatedFields(pages, childToGroup) {
1174
1266
  const kinds = /* @__PURE__ */ new Map();
1175
1267
  for (const page of pages) {
1176
1268
  for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
@@ -1190,6 +1282,10 @@ function validateCalculatedFields(pages) {
1190
1282
  for (const ref of valueRefs) {
1191
1283
  const kind = kinds.get(ref);
1192
1284
  if (kind === void 0) {
1285
+ const owningGroup = childToGroup.get(ref);
1286
+ if (owningGroup !== void 0) {
1287
+ return `Calculated field ${block.id} references ${ref}, which is a child of repeating group ${owningGroup} \u2014 calculations can't reach inside a repeating group (calc operands must be top-level fields)`;
1288
+ }
1193
1289
  return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
1194
1290
  }
1195
1291
  if (!CALC_OPERAND_KINDS.includes(kind)) {
@@ -1263,6 +1359,151 @@ function calcComplexityError(pages) {
1263
1359
  }
1264
1360
  return null;
1265
1361
  }
1362
+ var GROUP_CHILD_ALLOWED_KINDS = [
1363
+ "short_text",
1364
+ "long_text",
1365
+ "email",
1366
+ "url",
1367
+ "phone",
1368
+ "number",
1369
+ "select",
1370
+ "multi_select",
1371
+ "dropdown",
1372
+ "checkbox",
1373
+ "date",
1374
+ "rating",
1375
+ "linear_scale"
1376
+ ];
1377
+ var MAX_GROUP_TEMPLATE_CHILDREN = 12;
1378
+ var MAX_GROUP_ESTIMATED_BYTES = 18e4;
1379
+ function worstCaseChildBytes(child) {
1380
+ const overhead = child.id.length + 20;
1381
+ switch (child.kind) {
1382
+ case "short_text":
1383
+ case "email":
1384
+ case "url":
1385
+ return overhead + (child.maxLength ?? 2e3);
1386
+ case "long_text":
1387
+ return overhead + (child.maxLength ?? 2e4);
1388
+ case "phone":
1389
+ return overhead + 32;
1390
+ case "number":
1391
+ return overhead + 32;
1392
+ case "select":
1393
+ case "dropdown": {
1394
+ const longestOption = child.options.reduce((max, o) => Math.max(max, o.id.length), 0);
1395
+ return overhead + Math.max(longestOption, child.allowOther ? 500 : 0);
1396
+ }
1397
+ case "multi_select": {
1398
+ const everyOption = child.options.reduce((sum, o) => sum + o.id.length + 3, 0);
1399
+ return overhead + everyOption + (child.allowOther ? 500 : 0);
1400
+ }
1401
+ case "checkbox":
1402
+ return overhead + 8;
1403
+ case "date":
1404
+ return overhead + 16;
1405
+ case "rating":
1406
+ case "linear_scale":
1407
+ return overhead + 8;
1408
+ default:
1409
+ return overhead + 64;
1410
+ }
1411
+ }
1412
+ function groupSizeEstimate(field2) {
1413
+ const perInstance = field2.fields.reduce((sum, child) => sum + worstCaseChildBytes(child), 24);
1414
+ return perInstance * (Number.isFinite(field2.maxInstances) ? field2.maxInstances : 0);
1415
+ }
1416
+ function validateRepeatingGroups(pages) {
1417
+ for (const page of pages) {
1418
+ for (const block of page.blocks) {
1419
+ if (block.kind !== "repeating_group") continue;
1420
+ if (!Number.isInteger(block.maxInstances) || block.maxInstances < 1 || block.maxInstances > 20) {
1421
+ return `Repeating group ${block.id} needs maxInstances to be a whole number between 1 and 20`;
1422
+ }
1423
+ const minInstances = block.minInstances ?? 1;
1424
+ if (!Number.isInteger(minInstances) || minInstances < 0 || minInstances > block.maxInstances) {
1425
+ return `Repeating group ${block.id} needs minInstances to be a whole number between 0 and maxInstances (${block.maxInstances})`;
1426
+ }
1427
+ block.minInstances = minInstances;
1428
+ if (block.fields.length === 0) {
1429
+ return `Repeating group ${block.id} has no template fields \u2014 add at least one field to repeat`;
1430
+ }
1431
+ if (block.fields.length > MAX_GROUP_TEMPLATE_CHILDREN) {
1432
+ return `Repeating group ${block.id} has ${block.fields.length} template fields \u2014 the maximum is ${MAX_GROUP_TEMPLATE_CHILDREN}; split it into fewer fields`;
1433
+ }
1434
+ for (const child of block.fields) {
1435
+ if (!GROUP_CHILD_ALLOWED_KINDS.includes(child.kind)) {
1436
+ return `Repeating group ${block.id} has a "${child.kind}" child (${child.id}) \u2014 allowed child kinds are ${GROUP_CHILD_ALLOWED_KINDS.join(", ")}`;
1437
+ }
1438
+ }
1439
+ const duplicateChild = duplicateOptionId(block.fields);
1440
+ if (duplicateChild) {
1441
+ return `Duplicate child id in repeating group ${block.id}: ${duplicateChild}`;
1442
+ }
1443
+ const siblingIds = new Set(block.fields.map((f) => f.id));
1444
+ for (const child of block.fields) {
1445
+ for (const cond of child.visibleIf ?? []) {
1446
+ if (!siblingIds.has(cond.fieldId)) {
1447
+ return `Field ${child.id} in repeating group ${block.id} has a visibleIf referencing "${cond.fieldId}" \u2014 a child's visibleIf can only reference another field in the SAME group`;
1448
+ }
1449
+ }
1450
+ }
1451
+ const estimate = groupSizeEstimate(block);
1452
+ if (estimate > MAX_GROUP_ESTIMATED_BYTES) {
1453
+ return `Repeating group ${block.id} could reach ~${Math.ceil(estimate / 1e3)}KB at ${block.maxInstances} instances \u2014 the maximum is ~${Math.round(MAX_GROUP_ESTIMATED_BYTES / 1e3)}KB; lower maxInstances, shorten text limits, or trim the template`;
1454
+ }
1455
+ }
1456
+ }
1457
+ return null;
1458
+ }
1459
+ function collectGroupChildIds(pages) {
1460
+ const topLevelIds = /* @__PURE__ */ new Set();
1461
+ for (const page of pages) {
1462
+ for (const block of page.blocks) if (isField(block)) topLevelIds.add(block.id);
1463
+ }
1464
+ const childToGroup = /* @__PURE__ */ new Map();
1465
+ for (const page of pages) {
1466
+ for (const block of page.blocks) {
1467
+ if (block.kind !== "repeating_group") continue;
1468
+ for (const child of block.fields) {
1469
+ if (!topLevelIds.has(child.id) && !childToGroup.has(child.id)) {
1470
+ childToGroup.set(child.id, block.id);
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ return childToGroup;
1476
+ }
1477
+ function groupScopeWallError(pages, rawPages, childToGroup) {
1478
+ for (const page of pages) {
1479
+ for (const block of page.blocks) {
1480
+ for (const cond of block.visibleIf ?? []) {
1481
+ const owner = childToGroup.get(cond.fieldId);
1482
+ if (owner !== void 0) {
1483
+ return `Field ${block.id}'s visibleIf references "${cond.fieldId}", which is a child of repeating group ${owner} \u2014 fields outside a repeating group can't reference its children`;
1484
+ }
1485
+ }
1486
+ }
1487
+ }
1488
+ for (const page of rawPages) {
1489
+ if (!Array.isArray(page.next)) continue;
1490
+ for (const rule of page.next) {
1491
+ if (!rule || typeof rule !== "object") continue;
1492
+ const when2 = rule.when;
1493
+ if (!Array.isArray(when2)) continue;
1494
+ for (const cond of when2) {
1495
+ if (!cond || typeof cond !== "object") continue;
1496
+ const fieldId = cond.fieldId;
1497
+ if (typeof fieldId !== "string") continue;
1498
+ const owner = childToGroup.get(fieldId);
1499
+ if (owner !== void 0) {
1500
+ return `A jump rule on page ${page.id} references "${fieldId}", which is a child of repeating group ${owner} \u2014 jumps can't reference a repeating group's children`;
1501
+ }
1502
+ }
1503
+ }
1504
+ }
1505
+ return null;
1506
+ }
1266
1507
  function baseBlock(rec) {
1267
1508
  return {
1268
1509
  id: str(rec.id, 128),
@@ -1476,6 +1717,33 @@ function normalizeBlock(input) {
1476
1717
  return item ? [item] : [];
1477
1718
  }).slice(0, 50) : void 0
1478
1719
  });
1720
+ case "repeating_group": {
1721
+ const rawFields = Array.isArray(rec.fields) ? rec.fields : [];
1722
+ const fields = rawFields.slice(0, 50).flatMap((raw) => {
1723
+ const child = normalizeBlock(raw);
1724
+ return child && isField(child) ? [child] : [];
1725
+ });
1726
+ return cleanObject({
1727
+ ...baseField(rec, kind),
1728
+ // A container's OWN completeness is governed by minInstances (0 is a
1729
+ // legitimate "this group is optional"), not the generic required
1730
+ // flag — a separately-true `required` would be redundant at best and
1731
+ // self-contradictory at worst (the hidden/calculated precedent).
1732
+ required: false,
1733
+ fields,
1734
+ // Bounds become authoritative in validateRepeatingGroups as HARD
1735
+ // ERRORS, not silent clamps (unlike rating.max/linear_scale.min-max)
1736
+ // — instances are respondent-controlled wire shape, not cosmetic
1737
+ // display config. maxInstances is schema-REQUIRED, so a missing/
1738
+ // non-finite raw value becomes NaN here (fails every bounds check on
1739
+ // its own) rather than a fabricated fallback the post-pass would
1740
+ // have to override anyway.
1741
+ maxInstances: finite(rec.maxInstances) ?? Number.NaN,
1742
+ minInstances: finite(rec.minInstances),
1743
+ addLabel: optionalStr(rec.addLabel, 100),
1744
+ itemLabel: optionalStr(rec.itemLabel, 100)
1745
+ });
1746
+ }
1479
1747
  case "custom":
1480
1748
  return cleanObject({
1481
1749
  ...baseField(rec, kind),
@@ -1564,7 +1832,12 @@ function normalizeFormSchema(input) {
1564
1832
  }
1565
1833
  }
1566
1834
  }
1567
- const calcError = validateCalculatedFields(pages);
1835
+ const groupError = validateRepeatingGroups(pages);
1836
+ if (groupError) return { ok: false, error: groupError };
1837
+ const childToGroup = collectGroupChildIds(pages);
1838
+ const wallError = groupScopeWallError(pages, rebuilt, childToGroup);
1839
+ if (wallError) return { ok: false, error: wallError };
1840
+ const calcError = validateCalculatedFields(pages, childToGroup);
1568
1841
  if (calcError) return { ok: false, error: calcError };
1569
1842
  const calcComplexity = calcComplexityError(pages);
1570
1843
  if (calcComplexity) return { ok: false, error: calcComplexity };
@@ -1653,6 +1926,19 @@ function formatAnswer(field2, value) {
1653
1926
  const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
1654
1927
  return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
1655
1928
  }
1929
+ case "repeating_group": {
1930
+ if (!Array.isArray(value)) return String(value);
1931
+ const instances = value;
1932
+ if (instances.length === 0) return "";
1933
+ const noun = field2.itemLabel ?? field2.label;
1934
+ const summary = `${instances.length} \xD7 ${noun}`;
1935
+ const firstChild = field2.fields[0];
1936
+ if (!firstChild) return summary;
1937
+ const previews = instances.map(
1938
+ (instance) => instance && typeof instance === "object" && !Array.isArray(instance) ? formatAnswer(firstChild, instance[firstChild.id]) : ""
1939
+ ).filter((text) => text !== "");
1940
+ return previews.length > 0 ? `${summary}: ${previews.join(", ")}` : summary;
1941
+ }
1656
1942
  case "phone": {
1657
1943
  const { country, national, e164 } = parsePhone(String(value));
1658
1944
  if (!e164) return String(value);
@@ -1815,6 +2101,7 @@ var BLOCK_KIND_META = {
1815
2101
  file_upload: { label: "File upload", hint: "Direct to connected storage" },
1816
2102
  hidden: { label: "Hidden field", hint: "Filled from the URL" },
1817
2103
  calculated: { label: "Calculated value", hint: "Computed from other answers" },
2104
+ repeating_group: { label: "Repeating group", hint: "A set of fields respondents can repeat" },
1818
2105
  custom: { label: "Custom", hint: "Your own component (code only)" },
1819
2106
  heading: { label: "Heading", hint: "Section title" },
1820
2107
  paragraph: { label: "Paragraph", hint: "Explanatory text" },
@@ -1886,6 +2173,14 @@ function createBlock(kind) {
1886
2173
  return { id, kind, label: "Hidden field" };
1887
2174
  case "calculated":
1888
2175
  return { id, kind, label: "Calculated value", calc: { op: "const", value: 0 } };
2176
+ case "repeating_group":
2177
+ return {
2178
+ id,
2179
+ kind,
2180
+ label: "Repeating group",
2181
+ maxInstances: 5,
2182
+ fields: [{ id: createId(6), kind: "short_text", label: "Short text" }]
2183
+ };
1889
2184
  case "custom":
1890
2185
  return { id, kind, label: "Custom field", component: "" };
1891
2186
  case "file_upload":
@@ -2919,6 +3214,8 @@ function prefillFromParams(form, params) {
2919
3214
  case "calculated":
2920
3215
  break;
2921
3216
  // engine-computed, never prefillable — a crafted URL can't seed it
3217
+ case "repeating_group":
3218
+ break;
2922
3219
  default:
2923
3220
  data[field2.id] = raw;
2924
3221
  }
@@ -2927,6 +3224,22 @@ function prefillFromParams(form, params) {
2927
3224
  }
2928
3225
 
2929
3226
  // src/controller.ts
3227
+ function browserAttribution() {
3228
+ if (typeof document === "undefined" || typeof window === "undefined") return void 0;
3229
+ try {
3230
+ const page = new URL(window.location.href);
3231
+ page.hash = "";
3232
+ const hubspotutk = document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith("hubspotutk="))?.slice("hubspotutk=".length);
3233
+ const attribution = {
3234
+ pageUri: page.href.slice(0, 2048),
3235
+ pageName: document.title.trim().slice(0, 200) || void 0,
3236
+ hubspotutk: hubspotutk && /^[a-f0-9]{32}$/i.test(hubspotutk) ? hubspotutk : void 0
3237
+ };
3238
+ return Object.values(attribution).some(Boolean) ? attribution : void 0;
3239
+ } catch {
3240
+ return void 0;
3241
+ }
3242
+ }
2930
3243
  var EMPTY_PAGE = { id: "empty", blocks: [] };
2931
3244
  var warnedNoClient = false;
2932
3245
  var warnedNoFormId = false;
@@ -3347,8 +3660,11 @@ function createFormController(options2) {
3347
3660
  data = computeCalculated(form, { ...data, [fieldId]: value });
3348
3661
  onChange?.(data);
3349
3662
  submitError = void 0;
3350
- if (fieldId in errors) {
3351
- errors = Object.fromEntries(Object.entries(errors).filter(([k]) => k !== fieldId));
3663
+ const prefix = `${fieldId}.`;
3664
+ if (fieldId in errors || Object.keys(errors).some((k) => k.startsWith(prefix))) {
3665
+ errors = Object.fromEntries(
3666
+ Object.entries(errors).filter(([k]) => k !== fieldId && !k.startsWith(prefix))
3667
+ );
3352
3668
  }
3353
3669
  notify();
3354
3670
  scheduleDraftSave();
@@ -3482,6 +3798,7 @@ function createFormController(options2) {
3482
3798
  hp: options2.getHoneypot?.() ?? "",
3483
3799
  elapsedMs: Date.now() - mountedAt,
3484
3800
  surface: options2.surface ?? "headless",
3801
+ attribution: browserAttribution(),
3485
3802
  submissionKey,
3486
3803
  draft: submitDraft ? { id: submitDraft.id, token: submitDraft.token } : void 0,
3487
3804
  respondent,
@@ -3952,6 +4269,9 @@ var JSX_BLOCK_SPECS = [
3952
4269
  field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
3953
4270
  field("Hidden", "hidden", ["paramName", "defaultValue"]),
3954
4271
  field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
4272
+ field("RepeatingGroup", "repeating_group", ["minInstances", "maxInstances", "addLabel", "itemLabel"], {
4273
+ blockChildren: true
4274
+ }),
3955
4275
  field("Custom", "custom", ["component", "config"]),
3956
4276
  content("Heading", "heading", true),
3957
4277
  content("Paragraph", "paragraph", true),
@@ -4055,6 +4375,29 @@ function buildProps(spec, el) {
4055
4375
  );
4056
4376
  }
4057
4377
  out.__childText = parts.join("");
4378
+ } else if (spec.blockChildren && props.children !== void 0) {
4379
+ const childBlocks = flatten(props.children).map((child) => {
4380
+ const childSpec = classify(child);
4381
+ if (childSpec.kind === "__page" || childSpec.kind === "__option" || childSpec.content) {
4382
+ throw new FilloJsxError(
4383
+ "NON_FILLO_CHILD",
4384
+ `Fillo.${spec.name} "${id}" children must be field elements, found Fillo.${childSpec.name} \u2014 a repeating group's template holds answerable fields only`
4385
+ );
4386
+ }
4387
+ return buildProps(childSpec, child);
4388
+ });
4389
+ const childIds = /* @__PURE__ */ new Set();
4390
+ for (const child of childBlocks) {
4391
+ const childId = child.id;
4392
+ if (childIds.has(childId)) {
4393
+ throw new FilloJsxError(
4394
+ "DUPLICATE_ID",
4395
+ `Two fields in repeating group "${id}" share the id "${childId}" \u2014 child ids key instance answers and must be unique within the group`
4396
+ );
4397
+ }
4398
+ childIds.add(childId);
4399
+ }
4400
+ out.__childBlocks = childBlocks;
4058
4401
  } else if (props.children !== void 0) {
4059
4402
  throw new FilloJsxError(
4060
4403
  "NON_FILLO_CHILD",
@@ -4077,6 +4420,16 @@ function buildProps(spec, el) {
4077
4420
  if (spec.textChildren && out.text === void 0) {
4078
4421
  throw new FilloJsxError("TEXT_CHILD_REQUIRED", `Fillo.${spec.name} "${id}" has no text`);
4079
4422
  }
4423
+ if (spec.blockChildren) {
4424
+ if (out.__childBlocks === void 0) {
4425
+ throw new FilloJsxError(
4426
+ "TEXT_CHILD_REQUIRED",
4427
+ `Fillo.${spec.name} "${id}" has no fields \u2014 a repeating group needs at least one field element as a child (empty templates are invalid)`
4428
+ );
4429
+ }
4430
+ out.fields = out.__childBlocks;
4431
+ delete out.__childBlocks;
4432
+ }
4080
4433
  if (props.visibleIf !== void 0) {
4081
4434
  const conditions2 = Array.isArray(props.visibleIf) ? props.visibleIf : [props.visibleIf];
4082
4435
  out.visibleIf = conditions2;
@@ -4184,6 +4537,11 @@ function schemaFromJsx(children, meta) {
4184
4537
  assertUniqueOptionIds(block.id, block.rows, "matrix rows");
4185
4538
  assertUniqueOptionIds(block.id, block.columns, "matrix columns");
4186
4539
  }
4540
+ if (block.kind === "repeating_group") {
4541
+ for (const child of block.fields) {
4542
+ if ("options" in child) assertUniqueOptionIds(child.id, child.options, "options");
4543
+ }
4544
+ }
4187
4545
  }
4188
4546
  }
4189
4547
  return {
@@ -4281,6 +4639,7 @@ export {
4281
4639
  FILLO_CALC_MIN_SDK_VERSION,
4282
4640
  FILLO_CHALLENGE_MIN_SDK_VERSION,
4283
4641
  FILLO_DATA_ATTRS,
4642
+ FILLO_GROUP_MIN_SDK_VERSION,
4284
4643
  FILLO_MIN_SDK_VERSION,
4285
4644
  FILLO_SCHEMA_VERSION,
4286
4645
  FILLO_SDK_VERSION,
@@ -4362,6 +4721,7 @@ export {
4362
4721
  validateResponse,
4363
4722
  visibleBlocks,
4364
4723
  visibleFields,
4724
+ visibleGroupChildren,
4365
4725
  visiblePageBlocks,
4366
4726
  when
4367
4727
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/core",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "Form schema, validation, logic engine and JS client for Fillo. Framework-agnostic.",
5
5
  "license": "MIT",
6
6
  "keywords": [