@usefillo/core 0.5.1 → 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 +239 -14
  2. package/dist/index.js +659 -111
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -449,6 +449,11 @@ interface FilloClientOptions {
449
449
  * so they hit localhost in dev and fillo.so in prod. Not for embedding.
450
450
  */
451
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;
452
457
  fetch?: typeof fetch;
453
458
  }
454
459
  interface PublishedForm {
@@ -511,7 +516,23 @@ interface UploadFileOptions {
511
516
  }
512
517
  declare class FilloError extends Error {
513
518
  status?: number | undefined;
514
- 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;
515
536
  }
516
537
  declare class FilloClient {
517
538
  /** Server origin this client targets, normalized (no trailing slash). */
@@ -528,11 +549,7 @@ declare class FilloClient {
528
549
  * Upsert a code-defined form into the workspace identified by the client's
529
550
  * publishable key. Returns the canonical form id used for submissions.
530
551
  */
531
- syncForm(handle: string, schema: FormSchema, theme?: FormTheme | null): Promise<{
532
- formId: string;
533
- slug: string;
534
- branding?: FormBranding;
535
- }>;
552
+ syncForm(handle: string, schema: FormSchema, theme?: FormTheme | null): Promise<SyncFormResult>;
536
553
  /** Submit a response. Returns per-field errors instead of throwing on validation failure. */
537
554
  submit(formId: string, data: ResponseData, meta?: SubmitMeta): Promise<SubmitResult>;
538
555
  /**
@@ -648,13 +665,20 @@ interface FormControllerOptions {
648
665
  */
649
666
  skipValidation?: boolean;
650
667
  /**
651
- * Embedding surface. "headless" (a bare engine with no Fillo-rendered layout,
652
- * i.e. createFormController / <FilloProvider>) is a paid capability the server
653
- * enforces at submit. The framed renderers (<FilloForm>, renderForm) pass
654
- * "default" explicitly. Defaults to "headless" — a bare createFormController is
655
- * itself the headless gate.
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.
656
673
  */
657
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>;
658
682
  }
659
683
  interface FormControllerState {
660
684
  data: ResponseData;
@@ -670,6 +694,8 @@ interface FormControllerState {
670
694
  isLastPage: boolean;
671
695
  /** True while any file field is uploading. */
672
696
  uploading: boolean;
697
+ /** Human-readable message for the last failed submit; cleared on edit/retry. */
698
+ submitError?: string;
673
699
  }
674
700
  interface FormController {
675
701
  /** Stable snapshot — same reference until something changes (safe for useSyncExternalStore). */
@@ -700,11 +726,132 @@ interface FormController {
700
726
  }
701
727
  declare function createFormController(options: FormControllerOptions): FormController;
702
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
+
703
845
  /** Result of syncing a code-defined form: its canonical id, slug, and branding. */
704
846
  interface SyncedForm {
705
847
  formId: string;
706
848
  slug: string;
707
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;
708
855
  }
709
856
  /**
710
857
  * A form whose structure lives in user code. Framework renderers can show it
@@ -731,9 +878,87 @@ declare function isCodeForm(form: unknown): form is CodeForm;
731
878
  declare function contentHash(input: string): string;
732
879
  /**
733
880
  * Sync a code-defined form into a workspace at most once per session for the
734
- * 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)]}
735
951
  */
736
- 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;
737
962
 
738
963
  /**
739
964
  * Build initial response data from URL query parameters — Tally-style
@@ -779,4 +1004,4 @@ declare class Sha1 {
779
1004
  }
780
1005
  declare const sha1Base64: (bytes: Uint8Array) => string;
781
1006
 
782
- 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, countryByTimeZone, 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 };