@usefillo/core 0.5.0 → 0.6.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +259 -20
  2. package/dist/index.js +968 -172
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -39,8 +39,9 @@ interface PhoneField extends BaseField {
39
39
  kind: "phone";
40
40
  /**
41
41
  * Country selected when the form opens (ISO-3166 alpha-2, e.g. "US"). When
42
- * unset the renderer falls back to the respondent's browser locale, then the
43
- * first country in the list. The respondent can always change it.
42
+ * unset the renderer falls back to the respondent's browser timezone, browser
43
+ * locale, then the first country in the list. The respondent can always
44
+ * change it.
44
45
  */
45
46
  defaultCountry?: string;
46
47
  }
@@ -351,6 +352,7 @@ declare const PHONE_COUNTRIES: PhoneCountry[];
351
352
  /** Flag emoji from an ISO-3166 alpha-2 code (regional-indicator letters). */
352
353
  declare function flagEmoji(iso2: string): string;
353
354
  declare function countryByIso(iso2: string | undefined): PhoneCountry | undefined;
355
+ declare function countryByTimeZone(timeZone: string | undefined): PhoneCountry | undefined;
354
356
  /** Country whose dial code is the longest prefix of these digits (no "+"). */
355
357
  declare function countryByDialCode(digits: string): PhoneCountry | undefined;
356
358
  /** Keep only 0-9 (and a leading "+") from arbitrary input. */
@@ -447,6 +449,11 @@ interface FilloClientOptions {
447
449
  * so they hit localhost in dev and fillo.so in prod. Not for embedding.
448
450
  */
449
451
  sameOrigin?: boolean;
452
+ /**
453
+ * Target a different Fillo server (staging, tests, a proxy on your own
454
+ * domain). Defaults to the hosted API; wins over sameOrigin.
455
+ */
456
+ baseUrl?: string;
450
457
  fetch?: typeof fetch;
451
458
  }
452
459
  interface PublishedForm {
@@ -465,14 +472,19 @@ interface PublishedForm {
465
472
  /** Workspace branding state — absent means show the badge (default). */
466
473
  branding?: FormBranding;
467
474
  }
468
- interface SubmitResult {
469
- ok: boolean;
470
- responseId?: string;
475
+ type SubmitResult = {
476
+ ok: true;
477
+ responseId: string;
471
478
  /** True when the API accepted the request as an already-recorded visitor response. */
472
479
  duplicate?: boolean;
480
+ errors?: undefined;
481
+ } | {
482
+ ok: false;
473
483
  /** fieldId -> message when the server rejects the submission. */
474
- errors?: Record<string, string>;
475
- }
484
+ errors: Record<string, string>;
485
+ responseId?: undefined;
486
+ duplicate?: undefined;
487
+ };
476
488
  /** Anti-spam signals collected by the renderer. */
477
489
  interface SubmitMeta {
478
490
  /** Honeypot value — must be empty for humans. */
@@ -504,7 +516,23 @@ interface UploadFileOptions {
504
516
  }
505
517
  declare class FilloError extends Error {
506
518
  status?: number | undefined;
507
- constructor(message: string, status?: number | undefined);
519
+ /** Server-suggested wait (from a 429's Retry-After header), seconds. */
520
+ retryAfterSec?: number | undefined;
521
+ constructor(message: string, status?: number | undefined,
522
+ /** Server-suggested wait (from a 429's Retry-After header), seconds. */
523
+ retryAfterSec?: number | undefined);
524
+ }
525
+ /** Duck-typed — `instanceof` breaks when two SDK copies end up in one bundle. */
526
+ declare function isFilloError(err: unknown): err is FilloError;
527
+ interface SyncFormResult {
528
+ formId: string;
529
+ slug: string;
530
+ branding?: FormBranding;
531
+ /** Lifecycle on newer servers: a draft can't accept public responses yet. */
532
+ status?: "draft" | "published";
533
+ /** Changes were staged as a draft for a human to publish. */
534
+ staged?: boolean;
535
+ warning?: string;
508
536
  }
509
537
  declare class FilloClient {
510
538
  /** Server origin this client targets, normalized (no trailing slash). */
@@ -521,11 +549,7 @@ declare class FilloClient {
521
549
  * Upsert a code-defined form into the workspace identified by the client's
522
550
  * publishable key. Returns the canonical form id used for submissions.
523
551
  */
524
- syncForm(handle: string, schema: FormSchema, theme?: FormTheme | null): Promise<{
525
- formId: string;
526
- slug: string;
527
- branding?: FormBranding;
528
- }>;
552
+ syncForm(handle: string, schema: FormSchema, theme?: FormTheme | null): Promise<SyncFormResult>;
529
553
  /** Submit a response. Returns per-field errors instead of throwing on validation failure. */
530
554
  submit(formId: string, data: ResponseData, meta?: SubmitMeta): Promise<SubmitResult>;
531
555
  /**
@@ -561,6 +585,12 @@ declare class FilloClient {
561
585
  * Box; we return the new file id for /complete to verify server-side.
562
586
  */
563
587
  private boxUpload;
588
+ /**
589
+ * Caller signal (if any) combined with a fresh per-request timeout. Created per
590
+ * fetch so each retry attempt gets its own deadline; the caller's own abort
591
+ * still drives the resumable-upload cancel semantics.
592
+ */
593
+ private uploadSignal;
564
594
  /** Retry a request with exponential backoff; never retries an aborted upload. */
565
595
  private retry;
566
596
  /**
@@ -635,12 +665,20 @@ interface FormControllerOptions {
635
665
  */
636
666
  skipValidation?: boolean;
637
667
  /**
638
- * Embedding surface. "headless" (a bare engine with no Fillo-rendered layout,
639
- * i.e. createFormController / <FilloProvider>) is a paid capability the server
640
- * enforces at submit. The framed renderers (<FilloForm>, renderForm) are
641
- * "default". Defaults to "default".
668
+ * Embedding surface, recorded per response for measurement. "headless" = a
669
+ * bare engine with no Fillo-rendered layout (createFormController /
670
+ * <FilloProvider>); the framed renderers (<FilloForm>, renderForm) pass
671
+ * "default" explicitly. Defaults to "headless" — a bare createFormController
672
+ * is itself headless.
642
673
  */
643
674
  surface?: "default" | "headless";
675
+ /**
676
+ * Resolve the submission target at submit time when `formId` is still
677
+ * unset — e.g. re-run a code-form sync that failed at mount. Answers are
678
+ * held (never dropped) while it runs; failure sets `submitError` and the
679
+ * respondent can retry.
680
+ */
681
+ resolveFormId?: () => Promise<string>;
644
682
  }
645
683
  interface FormControllerState {
646
684
  data: ResponseData;
@@ -656,6 +694,8 @@ interface FormControllerState {
656
694
  isLastPage: boolean;
657
695
  /** True while any file field is uploading. */
658
696
  uploading: boolean;
697
+ /** Human-readable message for the last failed submit; cleared on edit/retry. */
698
+ submitError?: string;
659
699
  }
660
700
  interface FormController {
661
701
  /** Stable snapshot — same reference until something changes (safe for useSyncExternalStore). */
@@ -686,11 +726,132 @@ interface FormController {
686
726
  }
687
727
  declare function createFormController(options: FormControllerOptions): FormController;
688
728
 
729
+ /**
730
+ * The auto-submit decision, shared by every renderer (react, dom, and any
731
+ * future surface) so "one tap, no button" behaves identically everywhere.
732
+ */
733
+ declare function isAutoSubmitBlock(block: Block): boolean;
734
+ /**
735
+ * Whether an auto-submit form still needs a visible submit button. Only a
736
+ * single visible field that can auto-submit goes button-less; more than one
737
+ * visible field → always a button. (Pass the visible blocks.)
738
+ */
739
+ declare function needsExplicitSubmit(visible: Block[]): boolean;
740
+ interface AutoSubmitContext {
741
+ form: FormSchema;
742
+ data: ResponseData;
743
+ status: FormStatus;
744
+ isLastPage: boolean;
745
+ uploading: boolean;
746
+ }
747
+ declare function shouldAutoSubmit(field: Field, value: FieldValue, ctx: AutoSubmitContext): boolean;
748
+
749
+ /**
750
+ * The styling contract, shared by every renderer: named slots a consumer can
751
+ * attach classes to, and data-* attributes that expose state so utility CSS
752
+ * (Tailwind `data-[invalid]:…`) can react to it. Slot names and attribute
753
+ * names are public API — renames are breaking; additions are minors.
754
+ */
755
+ declare const FILLO_SLOTS: readonly ["root", "header", "title", "description", "pageTitle", "progress", "progressFill", "blocks", "field", "label", "fieldDescription", "control", "options", "option", "optionLabel", "error", "footer", "button", "success"];
756
+ type FilloSlot = (typeof FILLO_SLOTS)[number];
757
+ /** data-* names emitted alongside the stable fillo-* classes. */
758
+ declare const FILLO_DATA_ATTRS: {
759
+ /** Slot name, on every slot element: `data-fillo="control"`. */
760
+ readonly slot: "data-fillo";
761
+ /** Field kind, on the field wrapper: `data-kind="email"`. */
762
+ readonly kind: "data-kind";
763
+ /** Field id, on the field wrapper (pre-existing). */
764
+ readonly field: "data-field";
765
+ readonly invalid: "data-invalid";
766
+ readonly required: "data-required";
767
+ /** Selected option row / active scale step / active star. */
768
+ readonly selected: "data-selected";
769
+ /** Checkbox / toggle checked state. */
770
+ readonly checked: "data-checked";
771
+ readonly dragOver: "data-drag-over";
772
+ /** Engine status on the root: idle | submitting | submitted | error. */
773
+ readonly state: "data-state";
774
+ readonly page: "data-page";
775
+ readonly lastPage: "data-last-page";
776
+ };
777
+ /** State handed to `classNames` functions so classes can vary by it. */
778
+ interface SlotState {
779
+ slot: FilloSlot;
780
+ kind?: BlockKind;
781
+ fieldId?: string;
782
+ optionId?: string;
783
+ invalid?: boolean;
784
+ required?: boolean;
785
+ selected?: boolean;
786
+ checked?: boolean;
787
+ dragOver?: boolean;
788
+ status?: FormStatus;
789
+ /** "primary" | "ghost" on the button slot. */
790
+ variant?: string;
791
+ }
792
+ type SlotClass = string | ((state: SlotState) => string);
793
+ interface FilloAppearance {
794
+ /** Highest-precedence theme tokens (over prop/code/dashboard themes). */
795
+ theme?: FormTheme;
796
+ /** Class strings appended after the built-in fillo-* class, per slot. */
797
+ classNames?: Partial<Record<FilloSlot, SlotClass>>;
798
+ /** Per-field overrides, keyed by field id — appended after the slot class. */
799
+ fields?: Record<string, Partial<Record<FilloSlot, SlotClass>>>;
800
+ }
801
+ /**
802
+ * Resolve the consumer classes for a slot: general slot class first, then the
803
+ * per-field override. Returns "" when nothing applies. The badge is
804
+ * deliberately not a slot — appearance can never reach it.
805
+ */
806
+ declare function resolveSlotClass(appearance: FilloAppearance | undefined, state: SlotState): string;
807
+ /** Append resolved consumer classes to a base fillo-* class string. */
808
+ declare function slotClass(base: string, appearance: FilloAppearance | undefined, state: SlotState): string;
809
+
810
+ /**
811
+ * Every visitor-facing string the default renderers emit, overridable as a
812
+ * unit so a localized site never shows stray English at the submit moment.
813
+ * Schema-authored text (labels, descriptions, success copy set in settings)
814
+ * always wins over these defaults.
815
+ */
816
+ interface FilloStrings {
817
+ back: string;
818
+ next: string;
819
+ submit: string;
820
+ submitting: string;
821
+ uploading: string;
822
+ /** Suffix on non-required field labels. */
823
+ optional: string;
824
+ /** The "Other" free-text choice. */
825
+ other: string;
826
+ otherPrompt: string;
827
+ otherPlaceholder: string;
828
+ /** Dropdown placeholder when the field sets none. */
829
+ choosePlaceholder: string;
830
+ /** Success screen defaults — settings.successTitle/successMessage win. */
831
+ successTitle: string;
832
+ successMessage: string;
833
+ closed: string;
834
+ notLive: string;
835
+ /** Fallback when a submit fails without a server message. */
836
+ submitFailed: string;
837
+ loadFailedNotFound: string;
838
+ loadFailedNetwork: string;
839
+ loadFailed: string;
840
+ renderFailed: string;
841
+ }
842
+ declare const DEFAULT_STRINGS: FilloStrings;
843
+ declare function resolveStrings(overrides?: Partial<FilloStrings>): FilloStrings;
844
+
689
845
  /** Result of syncing a code-defined form: its canonical id, slug, and branding. */
690
846
  interface SyncedForm {
691
847
  formId: string;
692
848
  slug: string;
693
849
  branding?: FormBranding;
850
+ /** Lifecycle on newer servers: a draft can't accept public responses yet. */
851
+ status?: "draft" | "published";
852
+ /** Changes were staged as a draft for a human to publish. */
853
+ staged?: boolean;
854
+ warning?: string;
694
855
  }
695
856
  /**
696
857
  * A form whose structure lives in user code. Framework renderers can show it
@@ -717,9 +878,87 @@ declare function isCodeForm(form: unknown): form is CodeForm;
717
878
  declare function contentHash(input: string): string;
718
879
  /**
719
880
  * Sync a code-defined form into a workspace at most once per session for the
720
- * same client, handle, schema, and theme.
881
+ * same client, handle, schema, and theme. `bypassCache` forces the network —
882
+ * used by submit-time resolution, where a stale cached formId must not be
883
+ * trusted over a fresh sync.
884
+ */
885
+ declare function syncCodeForm(client: FilloClient, form: CodeForm, opts?: {
886
+ bypassCache?: boolean;
887
+ }): Promise<SyncedForm>;
888
+
889
+ /**
890
+ * JSX authoring: `<Fillo.Email id="email" …/>` elements are INERT descriptors —
891
+ * never rendered, only walked. The walk is a pure function over element-shaped
892
+ * objects ({type, props}) with zero react imports, so the same components and
893
+ * compiler work in the browser, in RSC-adjacent client modules, and in Node
894
+ * (CLI extraction). Output is the exact CodeForm defineForm() emits: the sync
895
+ * pipeline, server, and dashboard never see JSX.
896
+ *
897
+ * Emission is canonical and sparse (only authored props, fixed key order) —
898
+ * it feeds the pre-normalization content hash, so ANY change here re-syncs
899
+ * every deployed JSX form. The wire-format snapshot test guards it.
900
+ */
901
+ /** Stable error codes; messages carry the fix. Data-integrity errors throw in
902
+ * production too — a silently dropped duplicate id would corrupt response keys. */
903
+ declare class FilloJsxError extends Error {
904
+ code: string;
905
+ constructor(code: string, message: string);
906
+ }
907
+ declare const BRAND: unique symbol;
908
+ interface BlockSpec {
909
+ /** Component name, for error messages: Fillo.<name>. */
910
+ name: string;
911
+ kind: string;
912
+ /** Canonical emit order after id/kind; visibleIf always emits last. */
913
+ props: readonly string[];
914
+ /** Accepts <Fillo.Option> children as the options list. */
915
+ optionChildren?: boolean;
916
+ /** String children become the `text` prop (heading/paragraph). */
917
+ textChildren?: boolean;
918
+ /** Content blocks carry no label/required/etc. */
919
+ content?: boolean;
920
+ }
921
+ /** One entry per authorable block — the anti-fan-out manifest: components,
922
+ * walk emission, and the docs table all derive from it. */
923
+ declare const JSX_BLOCK_SPECS: readonly BlockSpec[];
924
+ type Branded = {
925
+ (props: unknown): never;
926
+ [BRAND]?: BlockSpec;
927
+ };
928
+ /** The inert authoring components, keyed by their Fillo.* name. */
929
+ declare const JSX_BLOCK_COMPONENTS: Record<string, Branded>;
930
+ /** Schema-relevant props on <Fillo.Form> — everything else (client, appearance,
931
+ * onSubmitted, …) is renderer configuration the walk must ignore. */
932
+ interface JsxFormMeta {
933
+ id: string;
934
+ title?: string;
935
+ description?: string;
936
+ settings?: FormSettings;
937
+ theme?: FormTheme;
938
+ }
939
+ declare function schemaFromJsx(children: unknown, meta: Omit<JsxFormMeta, "theme">): FormSchema;
940
+ /** Compile <Fillo.Form> props (or the element from Fillo.defineForm(jsx)) into
941
+ * the same CodeForm defineForm() produces — the sync pipeline sees no JSX. */
942
+ declare function codeFormFromJsx(meta: JsxFormMeta, children: unknown): CodeForm;
943
+
944
+ /**
945
+ * Typed builder for visibleIf conditions — pure data out, canonical key order
946
+ * ({fieldId, op, value?}) so it participates in content hashing unchanged.
947
+ * Conditions AND together (schema semantics); there is deliberately no OR.
948
+ *
949
+ * visibleIf={when("topic").eq("sales")}
950
+ * visibleIf={[when("topic").answered(), when("score").lt(5)]}
721
951
  */
722
- declare function syncCodeForm(client: FilloClient, form: CodeForm): Promise<SyncedForm>;
952
+ interface WhenBuilder {
953
+ eq(value: string | number | boolean): Condition;
954
+ neq(value: string | number | boolean): Condition;
955
+ contains(value: string | number | boolean): Condition;
956
+ gt(value: number): Condition;
957
+ lt(value: number): Condition;
958
+ answered(): Condition;
959
+ notAnswered(): Condition;
960
+ }
961
+ declare function when(fieldId: string): WhenBuilder;
723
962
 
724
963
  /**
725
964
  * Build initial response data from URL query parameters — Tally-style
@@ -765,4 +1004,4 @@ declare class Sha1 {
765
1004
  }
766
1005
  declare const sha1Base64: (bytes: Uint8Array) => string;
767
1006
 
768
- export { BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CustomField, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, FilloClient, type FilloClientOptions, FilloError, type FormBranding, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, type JsonValue, type LinearScaleField, type MatrixField, type NumberField, PHONE_COUNTRIES, type ParagraphBlock, type ParsedPhone, type PhoneCountry, type PhoneField, type ProvisionWorkspaceResult, type PublishedForm, type RankingField, type RatingField, type ResponseData, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SubmitMeta, type SubmitResult, type SyncedForm, type TextField, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, allFields, assembleForm, contentHash, countryByDialCode, countryByIso, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, flagEmoji, formatAnswer, formatNational, isBlockVisible, isCodeForm, isField, isPossiblePhone, normalizeFormSchema, normalizeFormTheme, parsePhone, pipeBlock, prefillFromParams, provisionWorkspace, resolveText, sha1Base64, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields };
1007
+ export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CustomField, DEFAULT_STRINGS, DRAFT_KINDS, type DateField, type DividerBlock, FILLO_DATA_ATTRS, FILLO_SCHEMA_VERSION, FILLO_SDK_VERSION, FILLO_SLOTS, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, type FilloAppearance, FilloClient, type FilloClientOptions, FilloError, FilloJsxError, 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 LinearScaleField, type MatrixField, type NumberField, PHONE_COUNTRIES, type ParagraphBlock, type ParsedPhone, type PhoneCountry, type PhoneField, type ProvisionWorkspaceResult, type PublishedForm, type RankingField, type RatingField, type ResponseData, type SchemaValidationResult, type SelectOption, Sha1, type SignatureField, type SlotClass, type SlotState, type SubmitMeta, type SubmitResult, type SyncFormResult, type SyncedForm, type TextField, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, type WhenBuilder, allFields, assembleForm, codeFormFromJsx, contentHash, countryByDialCode, countryByIso, countryByTimeZone, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, digitsOnly, flagEmoji, formatAnswer, formatNational, isAutoSubmitBlock, isBlockVisible, isCodeForm, isField, isFilloError, isPossiblePhone, needsExplicitSubmit, normalizeFormSchema, normalizeFormTheme, parsePhone, pipeBlock, prefillFromParams, provisionWorkspace, resolveSlotClass, resolveStrings, resolveText, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, when };