@usefillo/core 0.17.0 → 0.20.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 +204 -171
  2. package/dist/index.js +142 -77
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -400,7 +400,7 @@ interface FormSchema {
400
400
  settings: FormSettings;
401
401
  }
402
402
  interface FormTheme {
403
- /** Force default renderer colors, or leave unset/"auto" to follow the system. */
403
+ /** Omit to inherit the host page. Use "auto" to follow the visitor's system directly. */
404
404
  colorScheme?: "light" | "dark" | "auto";
405
405
  /** Accent for buttons, focus rings, selection. */
406
406
  primary?: string;
@@ -1027,7 +1027,7 @@ declare function assembleForm(spec: FormDraftSpec): FormSchema;
1027
1027
 
1028
1028
  interface FilloClientOptions {
1029
1029
  /**
1030
- * Publishable workspace key (pk_…) — safe to ship in client code. Required
1030
+ * Publishable project key (pk_…) — safe to ship in client code. Required
1031
1031
  * only for syncing code-defined forms into the workspace.
1032
1032
  */
1033
1033
  key?: string;
@@ -1176,7 +1176,7 @@ interface SubmitMeta {
1176
1176
  * webhooks, and integrations can say WHO answered.
1177
1177
  */
1178
1178
  interface FilloRespondent {
1179
- /** Your stable user/account id — the identity key within your workspace. */
1179
+ /** Your stable user/account id — the identity key within your project. */
1180
1180
  id: string;
1181
1181
  email?: string;
1182
1182
  name?: string;
@@ -1184,8 +1184,8 @@ interface FilloRespondent {
1184
1184
  traits?: Record<string, string | number | boolean>;
1185
1185
  /**
1186
1186
  * Identity verification (optional): hex HMAC-SHA256 of `id`, computed on
1187
- * YOUR server with the workspace identity secret from Fillo settings. Once
1188
- * the workspace holds a secret, Fillo records identity only with a valid
1187
+ * YOUR server with the project identity secret from Fillo settings. Once
1188
+ * the project holds a secret, Fillo records identity only with a valid
1189
1189
  * hash — never compute this in the browser or the secret leaks.
1190
1190
  */
1191
1191
  hash?: string;
@@ -1255,6 +1255,10 @@ declare function isFilloError(err: unknown): err is FilloError;
1255
1255
  interface SyncFormResult {
1256
1256
  formId: string;
1257
1257
  slug: string;
1258
+ /** Absolute Fillo dashboard URL for reviewing and publishing this form.
1259
+ * This is server-owned and intentionally independent of `client.baseUrl`,
1260
+ * which may point at an app-owned API proxy. */
1261
+ manageUrl?: string;
1258
1262
  branding?: FormBranding;
1259
1263
  /** Human-verification challenge to render before submit, when the LIVE form
1260
1264
  * requires one (staged changes don't gate until published). Absent = no
@@ -1318,7 +1322,7 @@ declare class FilloClient {
1318
1322
  /** Server origin this client targets, normalized (no trailing slash). */
1319
1323
  readonly baseUrl: string;
1320
1324
  private fetch;
1321
- /** Publishable workspace key, when configured. */
1325
+ /** Publishable project key, when configured. */
1322
1326
  readonly key?: string;
1323
1327
  constructor(options?: FilloClientOptions);
1324
1328
  private url;
@@ -1327,7 +1331,7 @@ declare class FilloClient {
1327
1331
  getForm(idOrSlug: string): Promise<PublishedForm>;
1328
1332
  /**
1329
1333
  * Resolve a code-defined form through the workspace identified by the
1330
- * client's publishable key. Depending on workspace policy, changed content
1334
+ * client's publishable key. Depending on project policy, changed content
1331
1335
  * may be staged for review or resolved to the authoritative live snapshot.
1332
1336
  * Returns the canonical form id used for submissions.
1333
1337
  */
@@ -1439,7 +1443,7 @@ declare class FilloClient {
1439
1443
  }
1440
1444
  declare function createClient(options?: FilloClientOptions): FilloClient;
1441
1445
  interface ProvisionWorkspaceResult {
1442
- /** Publishable key (pk_…) for the new workspace. Pass to createClient({ key }). */
1446
+ /** Publishable key (pk_…) for the new workspace's default project. Pass to createClient({ key }). */
1443
1447
  key: string;
1444
1448
  organizationId: string;
1445
1449
  /** The private workspace link is emailed directly; `url` stays null. */
@@ -1473,6 +1477,181 @@ declare function provisionWorkspace(options: {
1473
1477
  fetch?: typeof fetch;
1474
1478
  }): Promise<ProvisionWorkspaceResult>;
1475
1479
 
1480
+ /**
1481
+ * The default English validation message for a required field left empty.
1482
+ * `validateField` returns this exact sentinel (core has no strings context),
1483
+ * and renderer layers swap it for field-aware/localized copy. Keep it
1484
+ * identical to `DEFAULT_FIELD_STRINGS.required`.
1485
+ */
1486
+ declare const REQUIRED_FIELD_MESSAGE = "This field is required";
1487
+ /**
1488
+ * The form-chrome strings the default renderers emit, overridable as a unit so
1489
+ * a localized site never shows stray English at the submit moment.
1490
+ * Schema-authored text (labels, descriptions, success copy set in settings)
1491
+ * always wins over these defaults.
1492
+ *
1493
+ * Field-level and validation copy (required message, upload field, duplicate/
1494
+ * resume notices) lives in {@link FilloFieldStrings} — split out so this
1495
+ * documented, all-`string` surface stays stable while parametrized field
1496
+ * strings can be functions. The renderers resolve both together
1497
+ * ({@link FilloRendererStrings}); the `strings` prop overrides either.
1498
+ */
1499
+ interface FilloStrings {
1500
+ back: string;
1501
+ next: string;
1502
+ submit: string;
1503
+ submitting: string;
1504
+ uploading: string;
1505
+ /** Suffix on non-required field labels. */
1506
+ optional: string;
1507
+ /** The "Other" free-text choice. */
1508
+ other: string;
1509
+ otherPrompt: string;
1510
+ otherPlaceholder: string;
1511
+ /** Dropdown placeholder when the field sets none. */
1512
+ choosePlaceholder: string;
1513
+ /** Success screen defaults — settings.successTitle/successMessage win. */
1514
+ successTitle: string;
1515
+ successMessage: string;
1516
+ closed: string;
1517
+ notLive: string;
1518
+ /** Title of the not-open state for a draft/storage-blocked form in production. */
1519
+ notOpenTitle: string;
1520
+ /** Body of the not-open state. */
1521
+ notOpenBody: string;
1522
+ /** Title of the closed-flavor state (expired/capped workspace);
1523
+ * the body reuses `closed`. */
1524
+ closedTitle: string;
1525
+ /** Fallback when a submit fails without a server message. */
1526
+ submitFailed: string;
1527
+ loadFailedNotFound: string;
1528
+ loadFailedNetwork: string;
1529
+ loadFailed: string;
1530
+ renderFailed: string;
1531
+ /** Saved-progress notice when a draft restored earlier answers. */
1532
+ resumeNotice: string;
1533
+ /** Upsert-mode notice when the person's previous response was prefilled. */
1534
+ editNotice: string;
1535
+ /** The discard action next to the resume notice. */
1536
+ resumeStartOver: string;
1537
+ /** Shown when the human-verification widget can't load (script blocked). */
1538
+ challengeUnavailable: string;
1539
+ }
1540
+ interface RespondentErrorStrings {
1541
+ submitFailed: string;
1542
+ loadFailedNotFound: string;
1543
+ loadFailedNetwork: string;
1544
+ formUnavailable: string;
1545
+ formClosed: string;
1546
+ submitRateLimited: string;
1547
+ respondentUnrecognized: string;
1548
+ fileUnavailable: string;
1549
+ scopeMissing: string;
1550
+ challengeIncomplete: string;
1551
+ challengeRetry: string;
1552
+ reviewAnswers: string;
1553
+ }
1554
+ type RespondentErrorOverrides = Omit<RespondentErrorStrings, "submitFailed" | "loadFailedNotFound" | "loadFailedNetwork" | "formClosed">;
1555
+ declare const DEFAULT_STRINGS: FilloStrings;
1556
+ declare const DEFAULT_RESPONDENT_ERROR_STRINGS: RespondentErrorStrings;
1557
+ /** Translate the renderer's existing top-level fallbacks plus its optional
1558
+ * respondent-only overrides into the controller's complete error contract. */
1559
+ declare function respondentErrorStringsFor(strings: Pick<FilloStrings, "submitFailed" | "loadFailedNotFound" | "loadFailedNetwork" | "closed"> & Pick<FilloFieldStrings, "respondentErrors">): RespondentErrorStrings;
1560
+ /**
1561
+ * Field-level and validation strings the default renderers emit. Kept apart
1562
+ * from {@link FilloStrings} so parametrized entries can be functions (a
1563
+ * translation places the value where its grammar needs it) without widening
1564
+ * the documented all-`string` chrome surface. Also the growth point for new
1565
+ * strings in general — unlike `FilloStrings`, nothing depends on this being
1566
+ * an exhaustively-enumerated, closed set.
1567
+ */
1568
+ interface FilloFieldStrings {
1569
+ /** Optional, backward-compatible overrides for respondent error states that
1570
+ * do not already have a top-level renderer string. */
1571
+ respondentErrors?: Partial<RespondentErrorOverrides>;
1572
+ /** Generic/legacy validation copy for a required field left empty. Mirrors
1573
+ * REQUIRED_FIELD_MESSAGE and remains the fallback for custom/hidden fields. */
1574
+ required: string;
1575
+ /** Optional field-aware required copy. Existing `required` overrides keep
1576
+ * their legacy generic behavior unless this function is also supplied. */
1577
+ requiredForField?: (field: Field) => string;
1578
+ /** Notice when a spent/expired resume link couldn't restore progress. */
1579
+ resumeLinkExpired: string;
1580
+ /** Success-screen message when a verified identity re-submits and the form
1581
+ * keeps the first answer (a visible duplicate, not a fresh response). */
1582
+ alreadyAnswered: string;
1583
+ /** Retry control on a failed upload row. */
1584
+ uploadRetry: string;
1585
+ /** Accessible action names for upload rows. The filename is appended by the renderer. */
1586
+ uploadCancel: string;
1587
+ uploadRemove: string;
1588
+ uploadDismiss: string;
1589
+ /** Visible secondary status for an upload in progress. */
1590
+ uploadingFile: (percent: number, size: string) => string;
1591
+ /** Visible secondary status for a completed upload. */
1592
+ uploadedFile: (size: string) => string;
1593
+ /** Dropzone copy when uploads can't run because the form isn't connected. */
1594
+ uploadsDisabled: string;
1595
+ /** Dropzone copy for an explicit render-only preview, which never has transport. */
1596
+ uploadsRenderOnly: string;
1597
+ /** Dropzone copy when the server temporarily refuses new file sessions. */
1598
+ uploadsUnavailable: string;
1599
+ /** An upload request reached the server, but storage could not accept it. */
1600
+ uploadUnavailable: string;
1601
+ /** An upload attempt failed with no actionable server message. */
1602
+ uploadFailed: string;
1603
+ /** Dropzone call to action; `multiple` is true when several files are allowed. */
1604
+ dropzoneTitle: (multiple: boolean) => string;
1605
+ /** Dropzone hint stating the per-file size limit in MB. */
1606
+ dropzoneHint: (maxMb: number) => string;
1607
+ /** A file exceeded the per-file MB limit before upload started. */
1608
+ fileTooLarge: (maxMb: number) => string;
1609
+ /** Screen-reader status: N uploads in progress. */
1610
+ filesUploading: (count: number) => string;
1611
+ /** Screen-reader status: N uploads failed. */
1612
+ uploadsFailed: (count: number) => string;
1613
+ /** Screen-reader status: N uploads completed. */
1614
+ filesUploaded: (count: number) => string;
1615
+ /** Live-region announcement while a submit is in flight — for auto-submit
1616
+ * forms, which have no footer/button to show `submitting` on. */
1617
+ submittingAnnouncement: string;
1618
+ /** @deprecated Retained for localization compatibility. Default renderers
1619
+ * now focus the first invalid control and show field-aware inline guidance. */
1620
+ errorSummaryTitle: string;
1621
+ /** Live-region announcement after a ranking move: "«label», position n of m". */
1622
+ rankingPosition: (label: string, position: number, count: number) => string;
1623
+ /** Repeating group: the Add button's default label. */
1624
+ groupAdd: string;
1625
+ /** Repeating group instance heading — "«item» n of m". */
1626
+ groupInstanceLabel: (item: string, position: number, count: number) => string;
1627
+ /** Repeating group per-instance Remove button accessible label. */
1628
+ groupRemoveLabel: (item: string, position: number) => string;
1629
+ /** Live-region announcement after adding an instance. */
1630
+ groupInstanceAdded: (item: string, position: number, count: number) => string;
1631
+ /** Live-region announcement after removing an instance. */
1632
+ groupInstanceRemoved: (item: string, count: number) => string;
1633
+ /** Live-region announcement once a phone country-picker selection commits
1634
+ * (focus moves straight to the national input, so nothing else announces it). */
1635
+ phoneCountrySelected: (name: string) => string;
1636
+ /** Live-region announcement of the phone country-picker's filtered result
1637
+ * count as the respondent types in the search box. */
1638
+ phoneResultsCount: (count: number) => string;
1639
+ /** Accessible name/state for an empty signature canvas. */
1640
+ signatureEmpty: string;
1641
+ /** Accessible name/state for a signed signature canvas. */
1642
+ signatureSigned: string;
1643
+ }
1644
+ declare const DEFAULT_FIELD_STRINGS: FilloFieldStrings;
1645
+ /** Everything the default renderers can localize — chrome + field/validation. */
1646
+ type FilloRendererStrings = FilloStrings & FilloFieldStrings;
1647
+ /** Resolve the renderer copy for core's stable required-field sentinel. */
1648
+ declare function requiredFieldMessage(field: Field, strings: Pick<FilloFieldStrings, "required" | "requiredForField">): string;
1649
+ /** Merge overrides over the built-in chrome + field defaults. Accepts a partial
1650
+ * of the full renderer surface so the `strings` prop can override either set.
1651
+ * A legacy generic `required` override disables the new field-aware default
1652
+ * unless the caller explicitly supplies `requiredForField` too. */
1653
+ declare function resolveStrings(overrides?: Partial<FilloRendererStrings>): FilloRendererStrings;
1654
+
1476
1655
  type FormStatus = "idle" | "submitting" | "submitted" | "error";
1477
1656
  interface FormControllerOptions {
1478
1657
  form: FormSchema;
@@ -1521,6 +1700,10 @@ interface FormControllerOptions {
1521
1700
  * origins, or deployment commands.
1522
1701
  */
1523
1702
  verboseResolutionErrors?: boolean;
1703
+ /** Safe respondent-facing fallbacks used when an API or transport error is
1704
+ * intentionally hidden. Renderers pass their resolved `strings` overrides so
1705
+ * this boundary never replaces localized copy with the English defaults. */
1706
+ respondentErrorStrings?: Partial<RespondentErrorStrings>;
1524
1707
  /**
1525
1708
  * Host-app account context (identify()): who is filling this form, by your
1526
1709
  * own user id. Sent with the submission and recorded as an unverified
@@ -1631,6 +1814,8 @@ interface FormController {
1631
1814
  client?: FilloClient;
1632
1815
  /** Late-bind identify() context — host sessions often resolve after mount. */
1633
1816
  respondent?: FilloRespondent;
1817
+ /** Replace localized respondent-safe error copy after a locale change. */
1818
+ respondentErrorStrings?: Partial<RespondentErrorStrings>;
1634
1819
  }): void;
1635
1820
  /**
1636
1821
  * Persist any unsaved draft progress right now (settings.saveProgress
@@ -1785,16 +1970,15 @@ declare const FILLO_THEME_VARS: readonly [{
1785
1970
  readonly var: "--fillo-primary-contrast";
1786
1971
  }];
1787
1972
  /**
1788
- * Infer `colorScheme` for a `FormTheme` with `background` AND `text` set
1789
- * but no explicit `colorScheme`, from the background's WCAG relative
1790
- * luminance — fixes the foot-gun where `theme={{background, text}}` alone
1791
- * left the light palette's muted/border/control-bg/error/primary-contrast
1792
- * tokens in place, reading as near-white-on-white (≈1.10:1) on a dark
1793
- * author-chosen background.
1973
+ * Infer `colorScheme` for a `FormTheme` with a fixed hex `background` and no
1974
+ * fixed light/dark scheme. A fixed background cannot safely keep following
1975
+ * the visitor's OS: a light-only host viewed on a dark OS otherwise gets dark
1976
+ * controls on a white form (and the reverse for dark backgrounds).
1794
1977
  *
1795
- * Explicit `colorScheme` always wins (returned unchanged). Only one of
1796
- * `background`/`text` set, neither set, or a non-`#rgb`/`#rrggbb`
1797
- * `background` also pass through unchanged — no inference.
1978
+ * Explicit `light`/`dark` always wins. An absent scheme or `auto` is resolved
1979
+ * from the background's WCAG relative luminance, even when `text` is omitted,
1980
+ * so the remaining muted/border/control/error/contrast tokens stay readable.
1981
+ * A missing or non-`#rgb`/`#rrggbb` background passes through unchanged.
1798
1982
  *
1799
1983
  * Returns a `FormTheme`, not just the scheme, so a renderer can drop this
1800
1984
  * in ahead of its existing code with no other change:
@@ -1805,163 +1989,12 @@ declare const FILLO_THEME_VARS: readonly [{
1805
1989
  */
1806
1990
  declare function resolveThemeAppearance(theme: FormTheme | null): FormTheme | null;
1807
1991
 
1808
- /**
1809
- * The default English validation message for a required field left empty.
1810
- * `validateField` returns this exact sentinel (core has no strings context),
1811
- * and renderer layers swap it for field-aware/localized copy. Keep it
1812
- * identical to `DEFAULT_FIELD_STRINGS.required`.
1813
- */
1814
- declare const REQUIRED_FIELD_MESSAGE = "This field is required";
1815
- /**
1816
- * The form-chrome strings the default renderers emit, overridable as a unit so
1817
- * a localized site never shows stray English at the submit moment.
1818
- * Schema-authored text (labels, descriptions, success copy set in settings)
1819
- * always wins over these defaults.
1820
- *
1821
- * Field-level and validation copy (required message, upload field, duplicate/
1822
- * resume notices) lives in {@link FilloFieldStrings} — split out so this
1823
- * documented, all-`string` surface stays stable while parametrized field
1824
- * strings can be functions. The renderers resolve both together
1825
- * ({@link FilloRendererStrings}); the `strings` prop overrides either.
1826
- */
1827
- interface FilloStrings {
1828
- back: string;
1829
- next: string;
1830
- submit: string;
1831
- submitting: string;
1832
- uploading: string;
1833
- /** Suffix on non-required field labels. */
1834
- optional: string;
1835
- /** The "Other" free-text choice. */
1836
- other: string;
1837
- otherPrompt: string;
1838
- otherPlaceholder: string;
1839
- /** Dropdown placeholder when the field sets none. */
1840
- choosePlaceholder: string;
1841
- /** Success screen defaults — settings.successTitle/successMessage win. */
1842
- successTitle: string;
1843
- successMessage: string;
1844
- closed: string;
1845
- notLive: string;
1846
- /** Title of the not-open state for a draft/storage-blocked form in production. */
1847
- notOpenTitle: string;
1848
- /** Body of the not-open state. */
1849
- notOpenBody: string;
1850
- /** Title of the closed-flavor state (expired/capped workspace);
1851
- * the body reuses `closed`. */
1852
- closedTitle: string;
1853
- /** Fallback when a submit fails without a server message. */
1854
- submitFailed: string;
1855
- loadFailedNotFound: string;
1856
- loadFailedNetwork: string;
1857
- loadFailed: string;
1858
- renderFailed: string;
1859
- /** Saved-progress notice when a draft restored earlier answers. */
1860
- resumeNotice: string;
1861
- /** Upsert-mode notice when the person's previous response was prefilled. */
1862
- editNotice: string;
1863
- /** The discard action next to the resume notice. */
1864
- resumeStartOver: string;
1865
- /** Shown when the human-verification widget can't load (script blocked). */
1866
- challengeUnavailable: string;
1867
- }
1868
- declare const DEFAULT_STRINGS: FilloStrings;
1869
- /**
1870
- * Field-level and validation strings the default renderers emit. Kept apart
1871
- * from {@link FilloStrings} so parametrized entries can be functions (a
1872
- * translation places the value where its grammar needs it) without widening
1873
- * the documented all-`string` chrome surface. Also the growth point for new
1874
- * strings in general — unlike `FilloStrings`, nothing depends on this being
1875
- * an exhaustively-enumerated, closed set.
1876
- */
1877
- interface FilloFieldStrings {
1878
- /** Generic/legacy validation copy for a required field left empty. Mirrors
1879
- * REQUIRED_FIELD_MESSAGE and remains the fallback for custom/hidden fields. */
1880
- required: string;
1881
- /** Optional field-aware required copy. Existing `required` overrides keep
1882
- * their legacy generic behavior unless this function is also supplied. */
1883
- requiredForField?: (field: Field) => string;
1884
- /** Notice when a spent/expired resume link couldn't restore progress. */
1885
- resumeLinkExpired: string;
1886
- /** Success-screen message when a verified identity re-submits and the form
1887
- * keeps the first answer (a visible duplicate, not a fresh response). */
1888
- alreadyAnswered: string;
1889
- /** Retry control on a failed upload row. */
1890
- uploadRetry: string;
1891
- /** Accessible action names for upload rows. The filename is appended by the renderer. */
1892
- uploadCancel: string;
1893
- uploadRemove: string;
1894
- uploadDismiss: string;
1895
- /** Visible secondary status for an upload in progress. */
1896
- uploadingFile: (percent: number, size: string) => string;
1897
- /** Visible secondary status for a completed upload. */
1898
- uploadedFile: (size: string) => string;
1899
- /** Dropzone copy when uploads can't run because the form isn't connected. */
1900
- uploadsDisabled: string;
1901
- /** Dropzone copy for an explicit render-only preview, which never has transport. */
1902
- uploadsRenderOnly: string;
1903
- /** Dropzone copy when the server temporarily refuses new file sessions. */
1904
- uploadsUnavailable: string;
1905
- /** An upload request reached the server, but storage could not accept it. */
1906
- uploadUnavailable: string;
1907
- /** An upload attempt failed with no actionable server message. */
1908
- uploadFailed: string;
1909
- /** Dropzone call to action; `multiple` is true when several files are allowed. */
1910
- dropzoneTitle: (multiple: boolean) => string;
1911
- /** Dropzone hint stating the per-file size limit in MB. */
1912
- dropzoneHint: (maxMb: number) => string;
1913
- /** A file exceeded the per-file MB limit before upload started. */
1914
- fileTooLarge: (maxMb: number) => string;
1915
- /** Screen-reader status: N uploads in progress. */
1916
- filesUploading: (count: number) => string;
1917
- /** Screen-reader status: N uploads failed. */
1918
- uploadsFailed: (count: number) => string;
1919
- /** Screen-reader status: N uploads completed. */
1920
- filesUploaded: (count: number) => string;
1921
- /** Live-region announcement while a submit is in flight — for auto-submit
1922
- * forms, which have no footer/button to show `submitting` on. */
1923
- submittingAnnouncement: string;
1924
- /** @deprecated Retained for localization compatibility. Default renderers
1925
- * now focus the first invalid control and show field-aware inline guidance. */
1926
- errorSummaryTitle: string;
1927
- /** Live-region announcement after a ranking move: "«label», position n of m". */
1928
- rankingPosition: (label: string, position: number, count: number) => string;
1929
- /** Repeating group: the Add button's default label. */
1930
- groupAdd: string;
1931
- /** Repeating group instance heading — "«item» n of m". */
1932
- groupInstanceLabel: (item: string, position: number, count: number) => string;
1933
- /** Repeating group per-instance Remove button accessible label. */
1934
- groupRemoveLabel: (item: string, position: number) => string;
1935
- /** Live-region announcement after adding an instance. */
1936
- groupInstanceAdded: (item: string, position: number, count: number) => string;
1937
- /** Live-region announcement after removing an instance. */
1938
- groupInstanceRemoved: (item: string, count: number) => string;
1939
- /** Live-region announcement once a phone country-picker selection commits
1940
- * (focus moves straight to the national input, so nothing else announces it). */
1941
- phoneCountrySelected: (name: string) => string;
1942
- /** Live-region announcement of the phone country-picker's filtered result
1943
- * count as the respondent types in the search box. */
1944
- phoneResultsCount: (count: number) => string;
1945
- /** Accessible name/state for an empty signature canvas. */
1946
- signatureEmpty: string;
1947
- /** Accessible name/state for a signed signature canvas. */
1948
- signatureSigned: string;
1949
- }
1950
- declare const DEFAULT_FIELD_STRINGS: FilloFieldStrings;
1951
- /** Everything the default renderers can localize — chrome + field/validation. */
1952
- type FilloRendererStrings = FilloStrings & FilloFieldStrings;
1953
- /** Resolve the renderer copy for core's stable required-field sentinel. */
1954
- declare function requiredFieldMessage(field: Field, strings: Pick<FilloFieldStrings, "required" | "requiredForField">): string;
1955
- /** Merge overrides over the built-in chrome + field defaults. Accepts a partial
1956
- * of the full renderer surface so the `strings` prop can override either set.
1957
- * A legacy generic `required` override disables the new field-aware default
1958
- * unless the caller explicitly supplies `requiredForField` too. */
1959
- declare function resolveStrings(overrides?: Partial<FilloRendererStrings>): FilloRendererStrings;
1960
-
1961
1992
  /** Result of syncing a code-defined form: its canonical id, slug, and branding. */
1962
1993
  interface SyncedForm {
1963
1994
  formId: string;
1964
1995
  slug: string;
1996
+ /** Absolute Fillo dashboard URL for reviewing and publishing this form. */
1997
+ manageUrl?: string;
1965
1998
  branding?: FormBranding;
1966
1999
  /** Human-verification challenge to render before submit, when the LIVE form
1967
2000
  * requires one (staged changes don't gate until published). Absent = no
@@ -2201,4 +2234,4 @@ declare class Sha1 {
2201
2234
  }
2202
2235
  declare const sha1Base64: (bytes: Uint8Array) => string;
2203
2236
 
2204
- export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type ChallengeTheme, 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 };
2237
+ export { type AutoSubmitContext, BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CalcExpr, type CalculatedField, type ChallengeConfig, type ChallengeTheme, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CreatedDraft, type CustomField, DEFAULT_FIELD_STRINGS, DEFAULT_RESPONDENT_ERROR_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 RespondentErrorOverrides, type RespondentErrorStrings, 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, respondentErrorStringsFor, responseScopeValue, schemaFromJsx, sha1Base64, shouldAutoSubmit, slotClass, syncCodeForm, toE164, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields, visibleGroupChildren, visiblePageBlocks, when };
package/dist/index.js CHANGED
@@ -737,15 +737,39 @@ var DEFAULT_STRINGS = {
737
737
  notOpenBody: "The form owner is still setting things up. Please check back soon.",
738
738
  closedTitle: "Responses are closed",
739
739
  submitFailed: "This form can't submit right now. Please try again in a moment.",
740
- loadFailedNotFound: "Form not found \u2014 check the form id and that it's published.",
741
- loadFailedNetwork: "Couldn't reach the server \u2014 check your connection or CORS.",
740
+ loadFailedNotFound: "Form not found. Check the link or ask the form owner for help.",
741
+ loadFailedNetwork: "Couldn't reach the server \u2014 check your connection and try again.",
742
742
  loadFailed: "This form could not be loaded.",
743
743
  renderFailed: "This form could not be rendered.",
744
744
  resumeNotice: "Picked up where you left off.",
745
745
  editNotice: "You're updating your earlier response.",
746
746
  resumeStartOver: "Start over",
747
- challengeUnavailable: "The verification check couldn't load. Refresh the page, or check that challenges.cloudflare.com isn't blocked."
747
+ challengeUnavailable: "The verification check couldn't load. Refresh the page and try again."
748
748
  };
749
+ var DEFAULT_RESPONDENT_ERROR_STRINGS = {
750
+ submitFailed: DEFAULT_STRINGS.submitFailed,
751
+ loadFailedNotFound: DEFAULT_STRINGS.loadFailedNotFound,
752
+ loadFailedNetwork: DEFAULT_STRINGS.loadFailedNetwork,
753
+ formUnavailable: "This form is unavailable.",
754
+ formClosed: DEFAULT_STRINGS.closed,
755
+ submitRateLimited: "Too many responses are being submitted right now. Wait a moment, then try again.",
756
+ respondentUnrecognized: "This form couldn't verify who is responding. Ask the form owner for help.",
757
+ fileUnavailable: "One of the uploaded files is no longer available. Remove it and upload it again.",
758
+ scopeMissing: "We couldn't tell which entry this response belongs to. Answer the required field and try again.",
759
+ challengeIncomplete: "Please complete the verification check, then submit.",
760
+ challengeRetry: "That verification didn't go through. Please complete the check again and resubmit.",
761
+ reviewAnswers: "We couldn't submit \u2014 please review your answers."
762
+ };
763
+ function respondentErrorStringsFor(strings) {
764
+ return {
765
+ ...DEFAULT_RESPONDENT_ERROR_STRINGS,
766
+ ...strings.respondentErrors,
767
+ submitFailed: strings.submitFailed,
768
+ loadFailedNotFound: strings.loadFailedNotFound,
769
+ loadFailedNetwork: strings.loadFailedNetwork,
770
+ formClosed: strings.closed
771
+ };
772
+ }
749
773
  var REQUIRED_BY_KIND = {
750
774
  short_text: "Enter your answer",
751
775
  long_text: "Enter your answer",
@@ -1088,7 +1112,7 @@ var schemaShape = z2.object({
1088
1112
  });
1089
1113
  var MAX_SCHEMA_VERSION = 1;
1090
1114
  var FILLO_SCHEMA_VERSION = 1;
1091
- var FILLO_SDK_VERSION = true ? "0.17.0" : "0.0.0-dev";
1115
+ var FILLO_SDK_VERSION = true ? "0.20.0" : "0.0.0-dev";
1092
1116
  var FILLO_MIN_SDK_VERSION = "0.4.0";
1093
1117
  var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
1094
1118
  var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
@@ -2520,7 +2544,7 @@ var FilloClient = class {
2520
2544
  /** Server origin this client targets, normalized (no trailing slash). */
2521
2545
  baseUrl;
2522
2546
  fetch;
2523
- /** Publishable workspace key, when configured. */
2547
+ /** Publishable project key, when configured. */
2524
2548
  key;
2525
2549
  constructor(options2 = {}) {
2526
2550
  this.baseUrl = options2.baseUrl ? options2.baseUrl.replace(/\/$/, "") : options2.sameOrigin ? "" : DEFAULT_BASE_URL;
@@ -2556,7 +2580,7 @@ var FilloClient = class {
2556
2580
  }
2557
2581
  /**
2558
2582
  * Resolve a code-defined form through the workspace identified by the
2559
- * client's publishable key. Depending on workspace policy, changed content
2583
+ * client's publishable key. Depending on project policy, changed content
2560
2584
  * may be staged for review or resolved to the authoritative live snapshot.
2561
2585
  * Returns the canonical form id used for submissions.
2562
2586
  */
@@ -2594,8 +2618,16 @@ var FilloClient = class {
2594
2618
  throw asRequestError(err);
2595
2619
  }
2596
2620
  if (res.status === 422) {
2597
- const body2 = await res.json();
2598
- return { ok: false, errors: body2.errors };
2621
+ const body2 = await res.json().catch(() => null);
2622
+ if (body2?.errors && typeof body2.errors === "object" && !Array.isArray(body2.errors)) {
2623
+ return { ok: false, errors: body2.errors };
2624
+ }
2625
+ throw new FilloError(
2626
+ typeof body2?.error === "string" && body2.error ? body2.error : "Submit failed: 422",
2627
+ 422,
2628
+ retryAfterSec(res),
2629
+ typeof body2?.code === "string" && body2.code ? body2.code : void 0
2630
+ );
2599
2631
  }
2600
2632
  if (!res.ok) {
2601
2633
  const { message, code } = await errorDetails(res, `Submit failed: ${res.status}`);
@@ -2701,13 +2733,10 @@ var FilloClient = class {
2701
2733
  }
2702
2734
  /** Current state of an upload session — used to resume after interruption. */
2703
2735
  getUploadSession(sessionId, token, signal = AbortSignal.timeout(UPLOAD_CONTROL_TIMEOUT_MS)) {
2704
- return this.json(
2705
- `/uploads/${encodeURIComponent(sessionId)}`,
2706
- {
2707
- ...token ? { headers: { "X-Fillo-Upload-Token": token } } : {},
2708
- signal
2709
- }
2710
- );
2736
+ return this.json(`/uploads/${encodeURIComponent(sessionId)}`, {
2737
+ ...token ? { headers: { "X-Fillo-Upload-Token": token } } : {},
2738
+ signal
2739
+ });
2711
2740
  }
2712
2741
  /**
2713
2742
  * Provider-aware browser-direct upload. Creates a session, uses the storage
@@ -2738,21 +2767,18 @@ var FilloClient = class {
2738
2767
  } else {
2739
2768
  const requestId = createId(24);
2740
2769
  const requestToken = createId(32);
2741
- const createSession = () => this.json(
2742
- `/forms/${encodeURIComponent(formId)}/uploads`,
2743
- {
2744
- method: "POST",
2745
- body: JSON.stringify({
2746
- requestId,
2747
- uploadToken: requestToken,
2748
- fieldId: options2.fieldId,
2749
- fileName: name,
2750
- size: file.size,
2751
- mime
2752
- }),
2753
- signal: this.uploadSignal(options2, UPLOAD_CONTROL_TIMEOUT_MS)
2754
- }
2755
- );
2770
+ const createSession = () => this.json(`/forms/${encodeURIComponent(formId)}/uploads`, {
2771
+ method: "POST",
2772
+ body: JSON.stringify({
2773
+ requestId,
2774
+ uploadToken: requestToken,
2775
+ fieldId: options2.fieldId,
2776
+ fileName: name,
2777
+ size: file.size,
2778
+ mime
2779
+ }),
2780
+ signal: this.uploadSignal(options2, UPLOAD_CONTROL_TIMEOUT_MS)
2781
+ });
2756
2782
  const deadline = Date.now() + UPLOAD_CONTROL_TIMEOUT_MS;
2757
2783
  let backoffMs = 250;
2758
2784
  for (; ; ) {
@@ -2764,10 +2790,7 @@ var FilloClient = class {
2764
2790
  if (!isControlRetryable(error, true)) throw error;
2765
2791
  const remaining = deadline - Date.now();
2766
2792
  if (remaining <= 0) throw error;
2767
- await waitForRetry(
2768
- Math.min(backoffMs, remaining),
2769
- options2.signal
2770
- );
2793
+ await waitForRetry(Math.min(backoffMs, remaining), options2.signal);
2771
2794
  backoffMs = Math.min(backoffMs * 2, 5e3);
2772
2795
  }
2773
2796
  }
@@ -2777,7 +2800,8 @@ var FilloClient = class {
2777
2800
  uploadToken: session.token
2778
2801
  });
2779
2802
  if (session.status === "complete" && session.file) return session.file;
2780
- if (session.status === "aborted") throw new FilloError("This upload session was cancelled", 409);
2803
+ if (session.status === "aborted")
2804
+ throw new FilloError("This upload session was cancelled", 409);
2781
2805
  let completeBody;
2782
2806
  if (session.transport?.type === "gdrive") {
2783
2807
  await this.driveUploadLoop(session, session.transport.uploadUrl, file, options2);
@@ -2788,17 +2812,16 @@ var FilloClient = class {
2788
2812
  } else if (session.transport?.type === "box") {
2789
2813
  completeBody = await this.boxUpload(session.transport, file, options2);
2790
2814
  } else {
2791
- throw new FilloError("This form's storage doesn't support uploads \u2014 connect Drive, S3, or Box.");
2815
+ throw new FilloError(
2816
+ "This form's storage doesn't support uploads \u2014 connect Drive, S3, or Box."
2817
+ );
2792
2818
  }
2793
- const finalize = () => this.json(
2794
- `/uploads/${encodeURIComponent(session.id)}/complete`,
2795
- {
2796
- method: "POST",
2797
- headers: session.token ? { "X-Fillo-Upload-Token": session.token } : void 0,
2798
- ...completeBody ? { body: JSON.stringify(completeBody) } : {},
2799
- signal: this.uploadSignal(options2, UPLOAD_CONTROL_TIMEOUT_MS)
2800
- }
2801
- );
2819
+ const finalize = () => this.json(`/uploads/${encodeURIComponent(session.id)}/complete`, {
2820
+ method: "POST",
2821
+ headers: session.token ? { "X-Fillo-Upload-Token": session.token } : void 0,
2822
+ ...completeBody ? { body: JSON.stringify(completeBody) } : {},
2823
+ signal: this.uploadSignal(options2, UPLOAD_CONTROL_TIMEOUT_MS)
2824
+ });
2802
2825
  let done;
2803
2826
  try {
2804
2827
  done = await finalize();
@@ -2811,10 +2834,7 @@ var FilloClient = class {
2811
2834
  const reconciled = await this.getUploadSession(
2812
2835
  session.id,
2813
2836
  session.token,
2814
- this.uploadSignal(
2815
- options2,
2816
- Math.max(1, deadline - Date.now())
2817
- )
2837
+ this.uploadSignal(options2, Math.max(1, deadline - Date.now()))
2818
2838
  );
2819
2839
  if (reconciled.status === "complete" && reconciled.file) {
2820
2840
  return reconciled.file;
@@ -2829,10 +2849,7 @@ var FilloClient = class {
2829
2849
  }
2830
2850
  const remaining = deadline - Date.now();
2831
2851
  if (remaining <= 0) throw error;
2832
- await waitForRetry(
2833
- Math.min(backoffMs, remaining),
2834
- options2.signal
2835
- );
2852
+ await waitForRetry(Math.min(backoffMs, remaining), options2.signal);
2836
2853
  backoffMs = Math.min(backoffMs * 2, 5e3);
2837
2854
  }
2838
2855
  }
@@ -2894,7 +2911,8 @@ var FilloClient = class {
2894
2911
  throw new FilloError(`Box session failed: ${r.status}`, r.status);
2895
2912
  return r;
2896
2913
  });
2897
- if (!sessionRes.ok) throw new FilloError(`Box session failed: ${sessionRes.status}`, sessionRes.status);
2914
+ if (!sessionRes.ok)
2915
+ throw new FilloError(`Box session failed: ${sessionRes.status}`, sessionRes.status);
2898
2916
  const session = await sessionRes.json();
2899
2917
  const whole = new Sha1();
2900
2918
  const parts = [];
@@ -2929,7 +2947,11 @@ var FilloClient = class {
2929
2947
  const commitRes = await this.retry(options2, async () => {
2930
2948
  const r = await this.fetch(session.session_endpoints.commit, {
2931
2949
  method: "POST",
2932
- headers: { ...auth, "Content-Type": "application/json", Digest: `sha=${bytesToBase64(whole.digest())}` },
2950
+ headers: {
2951
+ ...auth,
2952
+ "Content-Type": "application/json",
2953
+ Digest: `sha=${bytesToBase64(whole.digest())}`
2954
+ },
2933
2955
  body: JSON.stringify({ parts }),
2934
2956
  signal: this.uploadSignal(options2)
2935
2957
  });
@@ -2937,7 +2959,8 @@ var FilloClient = class {
2937
2959
  throw new FilloError(`Box commit failed: ${r.status}`, r.status);
2938
2960
  return r;
2939
2961
  });
2940
- if (!commitRes.ok) throw new FilloError(`Box commit failed: ${commitRes.status}`, commitRes.status);
2962
+ if (!commitRes.ok)
2963
+ throw new FilloError(`Box commit failed: ${commitRes.status}`, commitRes.status);
2941
2964
  const committed = await commitRes.json();
2942
2965
  return { providerFileId: committed.entries?.[0]?.id };
2943
2966
  }
@@ -3029,7 +3052,10 @@ var FilloClient = class {
3029
3052
  report(offset);
3030
3053
  if (offset <= startedAt) {
3031
3054
  if (++stalls >= MAX_CHUNK_RETRIES) {
3032
- throw new FilloError("Drive upload stalled \u2014 the storage endpoint stopped accepting progress", 0);
3055
+ throw new FilloError(
3056
+ "Drive upload stalled \u2014 the storage endpoint stopped accepting progress",
3057
+ 0
3058
+ );
3033
3059
  }
3034
3060
  } else {
3035
3061
  stalls = 0;
@@ -3247,7 +3273,7 @@ function warnNoFormId() {
3247
3273
  if (warnedNoFormId) return;
3248
3274
  warnedNoFormId = true;
3249
3275
  console.warn(
3250
- "[fillo] This form has a client but no submission target \u2014 a bare schema can't sync. Wrap it in defineForm({ id, pages }) (or author it with <Fillo.Form id=\u2026>) so it gets a workspace handle, or pass formId for a dashboard-built form: https://fillo.so/docs"
3276
+ "[fillo] This form has a client but no submission target \u2014 a bare schema can't sync. Wrap it in defineForm({ id, pages }) (or author it with <Fillo.Form id=\u2026>) so it gets a project handle, or pass formId for a dashboard-built form: https://fillo.so/docs"
3251
3277
  );
3252
3278
  }
3253
3279
  function warnNoClient() {
@@ -3357,25 +3383,45 @@ function isDraftGone(err) {
3357
3383
  return isFilloError(err) && (err.status === 401 || err.status === 403 || err.status === 404);
3358
3384
  }
3359
3385
  var DRAFT_DEBOUNCE_MS = 1500;
3360
- var CHALLENGE_INCOMPLETE_MESSAGE = "Please complete the verification check, then submit.";
3361
- var CHALLENGE_RETRY_MESSAGE = "That verification didn't go through. Please complete the check again and resubmit.";
3362
- function submitFailureMessage(err) {
3363
- if (isFilloError(err) && err.status && err.status > 0 && err.message) return err.message;
3364
- return "Couldn't reach the server \u2014 check your connection and try again. If this keeps happening, a browser extension, firewall, or the site's Content-Security-Policy may be blocking the request.";
3365
- }
3366
- function syncResolutionFailureMessage(err, verbose) {
3386
+ function submitFailureMessage(err, strings) {
3387
+ if (isFilloError(err)) {
3388
+ const status = err.status ?? 0;
3389
+ if (status === 404) return strings.loadFailedNotFound;
3390
+ switch (err.code) {
3391
+ case "submit_rate_limited":
3392
+ return strings.submitRateLimited;
3393
+ case "workspace_unavailable":
3394
+ case "provision_closed":
3395
+ return strings.formClosed;
3396
+ case "respondent_unrecognized":
3397
+ return strings.respondentUnrecognized;
3398
+ case "invalid_file_reference":
3399
+ return strings.fileUnavailable;
3400
+ case "response_scope_missing":
3401
+ return strings.scopeMissing;
3402
+ }
3403
+ if (status > 0) return strings.submitFailed;
3404
+ }
3405
+ return strings.loadFailedNetwork;
3406
+ }
3407
+ function syncResolutionFailureMessage(err, verbose, strings) {
3367
3408
  if (isFilloError(err)) {
3368
3409
  const status = err.status ?? 0;
3369
3410
  if (verbose && status > 0 && err.message) {
3370
3411
  return err.code ? `${err.message} (${err.code})` : err.message;
3371
3412
  }
3372
3413
  const definitive = status > 0 && status < 500 && status !== 408 && status !== 429;
3373
- if (definitive) return "This form is unavailable.";
3414
+ if (definitive) return strings.formUnavailable;
3415
+ if (status > 0) return strings.submitFailed;
3374
3416
  }
3375
- return submitFailureMessage(err);
3417
+ return submitFailureMessage(err, strings);
3376
3418
  }
3377
3419
  function createFormController(options2) {
3378
3420
  const { onChange, onSubmitted } = options2;
3421
+ let respondentErrorStrings = {
3422
+ ...DEFAULT_RESPONDENT_ERROR_STRINGS,
3423
+ ...options2.respondentErrorStrings
3424
+ };
3379
3425
  const initial = normalizeFormSchema(options2.form);
3380
3426
  if (!initial.ok) throw new Error(`Invalid form schema: ${initial.error}`);
3381
3427
  let form = initial.schema;
@@ -3604,7 +3650,12 @@ function createFormController(options2) {
3604
3650
  try {
3605
3651
  if (ref) {
3606
3652
  try {
3607
- await c.saveDraft(ref.id, ref.token, payload, keepalive ? { keepalive: true } : void 0);
3653
+ await c.saveDraft(
3654
+ ref.id,
3655
+ ref.token,
3656
+ payload,
3657
+ keepalive ? { keepalive: true } : void 0
3658
+ );
3608
3659
  draftRef = ref;
3609
3660
  return;
3610
3661
  } catch (err) {
@@ -3772,7 +3823,8 @@ function createFormController(options2) {
3772
3823
  status = "idle";
3773
3824
  submitError = syncResolutionFailureMessage(
3774
3825
  err,
3775
- options2.verboseResolutionErrors === true
3826
+ options2.verboseResolutionErrors === true,
3827
+ respondentErrorStrings
3776
3828
  );
3777
3829
  notify();
3778
3830
  throw err;
@@ -3785,7 +3837,7 @@ function createFormController(options2) {
3785
3837
  const challengeToken = options2.challengeRequired ? options2.getChallengeToken?.() : void 0;
3786
3838
  if (options2.challengeRequired && !challengeToken) {
3787
3839
  status = "idle";
3788
- submitError = CHALLENGE_INCOMPLETE_MESSAGE;
3840
+ submitError = respondentErrorStrings.challengeIncomplete;
3789
3841
  notify();
3790
3842
  return;
3791
3843
  }
@@ -3807,12 +3859,12 @@ function createFormController(options2) {
3807
3859
  } catch (err) {
3808
3860
  status = "idle";
3809
3861
  if (isFilloError(err) && err.code === "challenge_failed") {
3810
- submitError = CHALLENGE_RETRY_MESSAGE;
3862
+ submitError = respondentErrorStrings.challengeRetry;
3811
3863
  options2.onChallengeFailed?.();
3812
3864
  notify();
3813
3865
  return;
3814
3866
  }
3815
- submitError = submitFailureMessage(err);
3867
+ submitError = submitFailureMessage(err, respondentErrorStrings);
3816
3868
  notify();
3817
3869
  throw err;
3818
3870
  }
@@ -3822,14 +3874,15 @@ function createFormController(options2) {
3822
3874
  (p) => visiblePageBlocks(form, p, data).some((b) => b.id in errors)
3823
3875
  );
3824
3876
  if (firstBad >= 0) pageIndex = firstBad;
3825
- else submitError = "We couldn't submit \u2014 please review your answers.";
3877
+ else submitError = respondentErrorStrings.reviewAnswers;
3826
3878
  status = "idle";
3827
3879
  notify();
3828
3880
  return;
3829
3881
  }
3830
3882
  submissionKind = res.duplicate ? "duplicate" : res.updated ? "updated" : "created";
3831
3883
  if (sessionId) client.reportProgress(sessionId, { completed: true });
3832
- if (visitorKey) markSubmitted(visitorSubmissionKeyId(form, formId, result.data), res.responseId);
3884
+ if (visitorKey)
3885
+ markSubmitted(visitorSubmissionKeyId(form, formId, result.data), res.responseId);
3833
3886
  discardDraftState();
3834
3887
  status = "submitted";
3835
3888
  notify();
@@ -3864,6 +3917,12 @@ function createFormController(options2) {
3864
3917
  if ("formId" in ctx) formId = ctx.formId;
3865
3918
  if ("client" in ctx) client = ctx.client;
3866
3919
  if ("respondent" in ctx) respondent = ctx.respondent;
3920
+ if (ctx.respondentErrorStrings) {
3921
+ respondentErrorStrings = {
3922
+ ...DEFAULT_RESPONDENT_ERROR_STRINGS,
3923
+ ...ctx.respondentErrorStrings
3924
+ };
3925
+ }
3867
3926
  if (ctx.form && ctx.form !== form) {
3868
3927
  const next2 = normalizeFormSchema(ctx.form);
3869
3928
  if (!next2.ok) throw new Error(`Invalid form schema: ${next2.error}`);
@@ -4073,7 +4132,9 @@ function relativeLuminance(token) {
4073
4132
  return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
4074
4133
  }
4075
4134
  function resolveThemeAppearance(theme) {
4076
- if (!theme || theme.colorScheme || !theme.background || !theme.text) return theme;
4135
+ if (!theme || theme.colorScheme === "light" || theme.colorScheme === "dark" || !theme.background) {
4136
+ return theme;
4137
+ }
4077
4138
  const luminance = relativeLuminance(theme.background);
4078
4139
  if (luminance == null) return theme;
4079
4140
  return { ...theme, colorScheme: luminance > DARK_LUMINANCE_THRESHOLD ? "light" : "dark" };
@@ -4140,14 +4201,14 @@ var isVolatileSync = (result) => Boolean(
4140
4201
  result.staged || result.resolvedSchema || result.syncError || result.accepting === false || result.uploadsAvailable === false
4141
4202
  );
4142
4203
  function storageKey(client, handle) {
4143
- return `fillo:sync:v4:${client.baseUrl}|${client.key}|${handle}`;
4204
+ return `fillo:sync:v5:${client.baseUrl}|${client.key}|${handle}`;
4144
4205
  }
4145
4206
  function readStoredSync(client, handle, hash) {
4146
4207
  try {
4147
4208
  const raw = globalThis.localStorage?.getItem(storageKey(client, handle));
4148
4209
  if (!raw) return null;
4149
4210
  const parsed = JSON.parse(raw);
4150
- if (typeof parsed.formId !== "string" || typeof parsed.slug !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number" || parsed.uploadFileSizeLimitMb !== void 0 && (typeof parsed.uploadFileSizeLimitMb !== "number" || !Number.isFinite(parsed.uploadFileSizeLimitMb) || parsed.uploadFileSizeLimitMb <= 0) || parsed.status !== void 0 && parsed.status !== "draft" && parsed.status !== "published")
4211
+ if (typeof parsed.formId !== "string" || typeof parsed.slug !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number" || parsed.uploadFileSizeLimitMb !== void 0 && (typeof parsed.uploadFileSizeLimitMb !== "number" || !Number.isFinite(parsed.uploadFileSizeLimitMb) || parsed.uploadFileSizeLimitMb <= 0) || parsed.manageUrl !== void 0 && typeof parsed.manageUrl !== "string" || parsed.status !== void 0 && parsed.status !== "draft" && parsed.status !== "published")
4151
4212
  return null;
4152
4213
  const ttl = syncTtlMs(parsed.status);
4153
4214
  if (Date.now() - parsed.ts > ttl) return null;
@@ -4165,6 +4226,7 @@ function writeStoredSync(client, handle, hash, r) {
4165
4226
  JSON.stringify({
4166
4227
  formId: r.formId,
4167
4228
  slug: r.slug,
4229
+ manageUrl: r.manageUrl,
4168
4230
  status: r.status,
4169
4231
  uploadFileSizeLimitMb: r.uploadFileSizeLimitMb,
4170
4232
  hash,
@@ -4193,6 +4255,7 @@ function syncCodeForm(client, form, opts) {
4193
4255
  const promise2 = Promise.resolve({
4194
4256
  formId: stored.formId,
4195
4257
  slug: stored.slug,
4258
+ manageUrl: stored.manageUrl,
4196
4259
  status: stored.status,
4197
4260
  uploadFileSizeLimitMb: stored.uploadFileSizeLimitMb
4198
4261
  });
@@ -4634,6 +4697,7 @@ export {
4634
4697
  BLOCK_KIND_META,
4635
4698
  CONTENT_KINDS,
4636
4699
  DEFAULT_FIELD_STRINGS,
4700
+ DEFAULT_RESPONDENT_ERROR_STRINGS,
4637
4701
  DEFAULT_STRINGS,
4638
4702
  DRAFT_KINDS,
4639
4703
  FILLO_CALC_MIN_SDK_VERSION,
@@ -4709,6 +4773,7 @@ export {
4709
4773
  resolveStrings,
4710
4774
  resolveText,
4711
4775
  resolveThemeAppearance,
4776
+ respondentErrorStringsFor,
4712
4777
  responseScopeValue,
4713
4778
  schemaFromJsx,
4714
4779
  sha1Base64,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/core",
3
- "version": "0.17.0",
3
+ "version": "0.20.0",
4
4
  "description": "Form schema, validation, logic engine and JS client for Fillo. Framework-agnostic.",
5
5
  "license": "MIT",
6
6
  "keywords": [