@10x-media/form-builder 0.1.0-beta.0 → 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +43 -1402
- package/dist/actions/buildActionBlocks.js +19 -0
- package/dist/actions/buildActionBlocks.js.map +1 -0
- package/dist/actions/builtin/confirmation.js +49 -0
- package/dist/actions/builtin/confirmation.js.map +1 -0
- package/dist/actions/builtin/emailTeam.js +46 -0
- package/dist/actions/builtin/emailTeam.js.map +1 -0
- package/dist/actions/builtin/index.d.ts +7 -0
- package/dist/actions/builtin/index.js +13 -0
- package/dist/actions/builtin/index.js.map +1 -0
- package/dist/actions/builtin/signedWebhook.js +49 -0
- package/dist/actions/builtin/signedWebhook.js.map +1 -0
- package/dist/actions/defineAction.d.ts +36 -0
- package/dist/actions/defineAction.js +6 -0
- package/dist/actions/defineAction.js.map +1 -0
- package/dist/actions/dispatch.js +48 -0
- package/dist/actions/dispatch.js.map +1 -0
- package/dist/actions/registry.d.ts +16 -0
- package/dist/actions/registry.js +16 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/actions/runActions.d.ts +9 -0
- package/dist/actions/runActions.js +34 -0
- package/dist/actions/runActions.js.map +1 -0
- package/dist/actions/sign.d.ts +7 -0
- package/dist/actions/sign.js +9 -0
- package/dist/actions/sign.js.map +1 -0
- package/dist/actions/task.js +82 -0
- package/dist/actions/task.js.map +1 -0
- package/dist/aggregation/aggregateResponses.d.ts +31 -0
- package/dist/aggregation/aggregateResponses.js +86 -0
- package/dist/aggregation/aggregateResponses.js.map +1 -0
- package/dist/aggregation/aggregateRows.d.ts +10 -0
- package/dist/aggregation/aggregateRows.js +64 -0
- package/dist/aggregation/aggregateRows.js.map +1 -0
- package/dist/aggregation/resolveResultsRequest.d.ts +32 -0
- package/dist/aggregation/resolveResultsRequest.js +56 -0
- package/dist/aggregation/resolveResultsRequest.js.map +1 -0
- package/dist/aggregation/types.d.ts +39 -0
- package/dist/calc/computeCalcFields.d.ts +15 -0
- package/dist/calc/computeCalcFields.js +27 -0
- package/dist/calc/computeCalcFields.js.map +1 -0
- package/dist/calc/evaluate.d.ts +8 -0
- package/dist/calc/evaluate.js +59 -0
- package/dist/calc/evaluate.js.map +1 -0
- package/dist/calc/normalizeCalc.d.ts +8 -0
- package/dist/calc/normalizeCalc.js +100 -0
- package/dist/calc/normalizeCalc.js.map +1 -0
- package/dist/calc/types.d.ts +28 -0
- package/dist/client/ConditionBuilder.js +121 -0
- package/dist/client/ConditionBuilder.js.map +1 -0
- package/dist/client/ConditionRow.js +150 -0
- package/dist/client/ConditionRow.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +15 -0
- package/dist/client/FlowBuilder.js +408 -0
- package/dist/client/FlowBuilder.js.map +1 -0
- package/dist/client/FormConditionField.d.ts +21 -0
- package/dist/client/FormConditionField.js +33 -0
- package/dist/client/FormConditionField.js.map +1 -0
- package/dist/client/synthesizeClientField.js +46 -0
- package/dist/client/synthesizeClientField.js.map +1 -0
- package/dist/collections/formSubmissions.js +138 -0
- package/dist/collections/formSubmissions.js.map +1 -0
- package/dist/collections/forms.js +187 -0
- package/dist/collections/forms.js.map +1 -0
- package/dist/collections/uploads.d.ts +29 -0
- package/dist/collections/uploads.js +46 -0
- package/dist/collections/uploads.js.map +1 -0
- package/dist/conditions/conditionType.js +14 -0
- package/dist/conditions/conditionType.js.map +1 -0
- package/dist/conditions/evaluate.d.ts +17 -0
- package/dist/conditions/evaluate.js +94 -0
- package/dist/conditions/evaluate.js.map +1 -0
- package/dist/conditions/fieldTypes.d.ts +9 -0
- package/dist/{fieldTypes-CzhhJXjg.js → conditions/fieldTypes.js} +2 -2
- package/dist/conditions/fieldTypes.js.map +1 -0
- package/dist/conditions/normalizeConditions.js +47 -0
- package/dist/conditions/normalizeConditions.js.map +1 -0
- package/dist/conditions/types.d.ts +8 -0
- package/dist/consent/buildConsentSourceConfig.js +49 -0
- package/dist/consent/buildConsentSourceConfig.js.map +1 -0
- package/dist/consent/builtin/index.d.ts +7 -0
- package/dist/consent/builtin/index.js +11 -0
- package/dist/consent/builtin/index.js.map +1 -0
- package/dist/consent/builtin/pageReference.js +75 -0
- package/dist/consent/builtin/pageReference.js.map +1 -0
- package/dist/consent/builtin/static.js +44 -0
- package/dist/consent/builtin/static.js.map +1 -0
- package/dist/consent/captureConsent.d.ts +28 -0
- package/dist/consent/captureConsent.js +26 -0
- package/dist/consent/captureConsent.js.map +1 -0
- package/dist/consent/defineConsentSource.d.ts +35 -0
- package/dist/consent/defineConsentSource.js +6 -0
- package/dist/consent/defineConsentSource.js.map +1 -0
- package/dist/consent/registry.d.ts +16 -0
- package/dist/consent/registry.js +16 -0
- package/dist/consent/registry.js.map +1 -0
- package/dist/consent/resolveConsentLinks.d.ts +19 -0
- package/dist/consent/resolveConsentLinks.js +26 -0
- package/dist/consent/resolveConsentLinks.js.map +1 -0
- package/dist/consent/resolvePublishedVersionRef.d.ts +17 -0
- package/dist/consent/resolvePublishedVersionRef.js +27 -0
- package/dist/consent/resolvePublishedVersionRef.js.map +1 -0
- package/dist/events/noopSink.js +6 -0
- package/dist/events/noopSink.js.map +1 -0
- package/dist/events/resolveEventSink.js +8 -0
- package/dist/events/resolveEventSink.js.map +1 -0
- package/dist/events/types.d.ts +31 -0
- package/dist/exports/client.d.ts +3 -21
- package/dist/exports/client.js +3 -319
- package/dist/exports/i18n.d.ts +3 -138
- package/dist/exports/i18n.js +2 -2
- package/dist/exports/react.d.ts +45 -550
- package/dist/exports/react.js +39 -1563
- package/dist/exports/rsc.d.ts +2 -16
- package/dist/exports/rsc.js +1 -54
- package/dist/exports/types.d.ts +27 -5
- package/dist/fields/buildFieldBlocks.js +46 -0
- package/dist/fields/buildFieldBlocks.js.map +1 -0
- package/dist/fields/builtin/calculation.js +24 -0
- package/dist/fields/builtin/calculation.js.map +1 -0
- package/dist/fields/builtin/checkbox.js +13 -0
- package/dist/fields/builtin/checkbox.js.map +1 -0
- package/dist/fields/builtin/consent.js +32 -0
- package/dist/fields/builtin/consent.js.map +1 -0
- package/dist/fields/builtin/email.js +18 -0
- package/dist/fields/builtin/email.js.map +1 -0
- package/dist/fields/builtin/file.js +40 -0
- package/dist/fields/builtin/file.js.map +1 -0
- package/dist/fields/builtin/index.js +25 -0
- package/dist/fields/builtin/index.js.map +1 -0
- package/dist/fields/builtin/number.js +17 -0
- package/dist/fields/builtin/number.js.map +1 -0
- package/dist/fields/builtin/select.js +44 -0
- package/dist/fields/builtin/select.js.map +1 -0
- package/dist/fields/builtin/text.js +13 -0
- package/dist/fields/builtin/text.js.map +1 -0
- package/dist/fields/builtin/textarea.js +13 -0
- package/dist/fields/builtin/textarea.js.map +1 -0
- package/dist/fields/defineFormField.d.ts +13 -0
- package/dist/fields/defineFormField.js +12 -0
- package/dist/fields/defineFormField.js.map +1 -0
- package/dist/fields/registry.d.ts +10 -0
- package/dist/fields/registry.js +24 -0
- package/dist/fields/registry.js.map +1 -0
- package/dist/fields/sharedConfig.js +89 -0
- package/dist/fields/sharedConfig.js.map +1 -0
- package/dist/fields/types.d.ts +88 -0
- package/dist/flow/engine.d.ts +17 -0
- package/dist/flow/engine.js +24 -0
- package/dist/flow/engine.js.map +1 -0
- package/dist/flow/normalizeFlow.js +35 -0
- package/dist/flow/normalizeFlow.js.map +1 -0
- package/dist/{types-DsJ_6kGJ.d.ts → flow/types.d.ts} +2 -2
- package/dist/index.d.ts +74 -4
- package/dist/index.js +40 -1889
- package/dist/index.js.map +1 -1
- package/dist/plugin/access.js +12 -0
- package/dist/plugin/access.js.map +1 -0
- package/dist/plugin/registerCollections.js +40 -0
- package/dist/plugin/registerCollections.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/prefill/valuesFromSearchParams.d.ts +18 -0
- package/dist/prefill/valuesFromSearchParams.js +45 -0
- package/dist/prefill/valuesFromSearchParams.js.map +1 -0
- package/dist/presentations/defaults.d.ts +33 -0
- package/dist/presentations/defaults.js +35 -0
- package/dist/presentations/defaults.js.map +1 -0
- package/dist/presentations/registry.d.ts +15 -0
- package/dist/presentations/registry.js +15 -0
- package/dist/presentations/registry.js.map +1 -0
- package/dist/presentations/types.d.ts +18 -0
- package/dist/react/Form.d.ts +81 -0
- package/dist/react/Form.js +457 -0
- package/dist/react/Form.js.map +1 -0
- package/dist/react/FormContext.d.ts +16 -0
- package/dist/react/FormContext.js +14 -0
- package/dist/react/FormContext.js.map +1 -0
- package/dist/react/FormLayout.d.ts +20 -0
- package/dist/react/FormLayout.js +14 -0
- package/dist/react/FormLayout.js.map +1 -0
- package/dist/react/FormResults.d.ts +25 -0
- package/dist/react/FormResults.js +85 -0
- package/dist/react/FormResults.js.map +1 -0
- package/dist/react/Honeypot.d.ts +22 -0
- package/dist/react/Honeypot.js +38 -0
- package/dist/react/Honeypot.js.map +1 -0
- package/dist/react/Poll.d.ts +32 -0
- package/dist/react/Poll.js +77 -0
- package/dist/react/Poll.js.map +1 -0
- package/dist/react/cn.d.ts +6 -0
- package/dist/react/cn.js +8 -0
- package/dist/react/cn.js.map +1 -0
- package/dist/react/contract.d.ts +32 -0
- package/dist/react/contract.js +7 -0
- package/dist/react/contract.js.map +1 -0
- package/dist/react/events.js +14 -0
- package/dist/react/events.js.map +1 -0
- package/dist/react/fetchResults.d.ts +25 -0
- package/dist/react/fetchResults.js +31 -0
- package/dist/react/fetchResults.js.map +1 -0
- package/dist/react/presentation/Backdrop.d.ts +18 -0
- package/dist/react/presentation/Backdrop.js +14 -0
- package/dist/react/presentation/Backdrop.js.map +1 -0
- package/dist/react/presentation/DialogSurface.d.ts +35 -0
- package/dist/react/presentation/DialogSurface.js +56 -0
- package/dist/react/presentation/DialogSurface.js.map +1 -0
- package/dist/react/presentation/presentations.d.ts +7 -0
- package/dist/react/presentation/presentations.js +32 -0
- package/dist/react/presentation/presentations.js.map +1 -0
- package/dist/react/presentation/registry.d.ts +11 -0
- package/dist/react/presentation/registry.js +12 -0
- package/dist/react/presentation/registry.js.map +1 -0
- package/dist/react/presentation/types.d.ts +19 -0
- package/dist/react/presentation/useDismiss.d.ts +19 -0
- package/dist/react/presentation/useDismiss.js +33 -0
- package/dist/react/presentation/useDismiss.js.map +1 -0
- package/dist/react/presentation/useFocusTrap.d.ts +8 -0
- package/dist/react/presentation/useFocusTrap.js +35 -0
- package/dist/react/presentation/useFocusTrap.js.map +1 -0
- package/dist/react/presentation/useScrollLock.d.ts +6 -0
- package/dist/react/presentation/useScrollLock.js +18 -0
- package/dist/react/presentation/useScrollLock.js.map +1 -0
- package/dist/react/primitives/Checkbox.d.ts +26 -0
- package/dist/react/primitives/Checkbox.js +20 -0
- package/dist/react/primitives/Checkbox.js.map +1 -0
- package/dist/react/primitives/FieldShell.d.ts +31 -0
- package/dist/react/primitives/FieldShell.js +51 -0
- package/dist/react/primitives/FieldShell.js.map +1 -0
- package/dist/react/primitives/Input.d.ts +30 -0
- package/dist/react/primitives/Input.js +21 -0
- package/dist/react/primitives/Input.js.map +1 -0
- package/dist/react/primitives/Select.d.ts +34 -0
- package/dist/react/primitives/Select.js +26 -0
- package/dist/react/primitives/Select.js.map +1 -0
- package/dist/react/primitives/Textarea.d.ts +28 -0
- package/dist/react/primitives/Textarea.js +20 -0
- package/dist/react/primitives/Textarea.js.map +1 -0
- package/dist/react/recall.js +20 -0
- package/dist/react/recall.js.map +1 -0
- package/dist/react/registry.d.ts +16 -0
- package/dist/react/registry.js +16 -0
- package/dist/react/registry.js.map +1 -0
- package/dist/react/renderers/calculation.js +30 -0
- package/dist/react/renderers/calculation.js.map +1 -0
- package/dist/react/renderers/checkbox.js +35 -0
- package/dist/react/renderers/checkbox.js.map +1 -0
- package/dist/react/renderers/consent.js +54 -0
- package/dist/react/renderers/consent.js.map +1 -0
- package/dist/react/renderers/email.js +37 -0
- package/dist/react/renderers/email.js.map +1 -0
- package/dist/react/renderers/file.js +92 -0
- package/dist/react/renderers/file.js.map +1 -0
- package/dist/react/renderers/index.d.ts +8 -0
- package/dist/react/renderers/index.js +26 -0
- package/dist/react/renderers/index.js.map +1 -0
- package/dist/react/renderers/number.js +40 -0
- package/dist/react/renderers/number.js.map +1 -0
- package/dist/react/renderers/select.js +38 -0
- package/dist/react/renderers/select.js.map +1 -0
- package/dist/react/renderers/text.js +37 -0
- package/dist/react/renderers/text.js.map +1 -0
- package/dist/react/renderers/textarea.js +36 -0
- package/dist/react/renderers/textarea.js.map +1 -0
- package/dist/react/resolveForm.js +16 -0
- package/dist/react/resolveForm.js.map +1 -0
- package/dist/react/state.d.ts +15 -0
- package/dist/react/state.js +70 -0
- package/dist/react/state.js.map +1 -0
- package/dist/react/submitForm.d.ts +31 -0
- package/dist/react/submitForm.js +58 -0
- package/dist/react/submitForm.js.map +1 -0
- package/dist/react/toFormDocument.d.ts +24 -0
- package/dist/react/toFormDocument.js +22 -0
- package/dist/react/toFormDocument.js.map +1 -0
- package/dist/react/uploadFile.d.ts +23 -0
- package/dist/react/uploadFile.js +39 -0
- package/dist/react/uploadFile.js.map +1 -0
- package/dist/react/useField.d.ts +14 -0
- package/dist/react/useField.js +49 -0
- package/dist/react/useField.js.map +1 -0
- package/dist/react/useFormState.d.ts +8 -0
- package/dist/react/useFormState.js +9 -0
- package/dist/react/useFormState.js.map +1 -0
- package/dist/react/useFormStep.d.ts +8 -0
- package/dist/react/useFormStep.js +9 -0
- package/dist/react/useFormStep.js.map +1 -0
- package/dist/react/validateField.js +30 -0
- package/dist/react/validateField.js.map +1 -0
- package/dist/recall/interpolate.d.ts +10 -0
- package/dist/recall/interpolate.js +16 -0
- package/dist/recall/interpolate.js.map +1 -0
- package/dist/recall/resolver.d.ts +22 -0
- package/dist/recall/resolver.js +36 -0
- package/dist/recall/resolver.js.map +1 -0
- package/dist/spam/captcha.d.ts +12 -0
- package/dist/spam/captcha.js +11 -0
- package/dist/spam/captcha.js.map +1 -0
- package/dist/spam/constants.d.ts +12 -0
- package/dist/spam/constants.js +15 -0
- package/dist/spam/constants.js.map +1 -0
- package/dist/spam/identify.d.ts +12 -0
- package/dist/spam/identify.js +20 -0
- package/dist/spam/identify.js.map +1 -0
- package/dist/spam/rateLimiter.d.ts +18 -0
- package/dist/spam/rateLimiter.js +41 -0
- package/dist/spam/rateLimiter.js.map +1 -0
- package/dist/spam/reserved.js +30 -0
- package/dist/spam/reserved.js.map +1 -0
- package/dist/spam/resolveSpam.d.ts +12 -0
- package/dist/spam/resolveSpam.js +42 -0
- package/dist/spam/resolveSpam.js.map +1 -0
- package/dist/spam/spamGuard.js +68 -0
- package/dist/spam/spamGuard.js.map +1 -0
- package/dist/spam/types.d.ts +73 -0
- package/dist/spam/uploadHooks.js +33 -0
- package/dist/spam/uploadHooks.js.map +1 -0
- package/dist/submissions/SubmissionAnswers.d.ts +16 -0
- package/dist/submissions/SubmissionAnswers.js +57 -0
- package/dist/submissions/SubmissionAnswers.js.map +1 -0
- package/dist/submissions/runSubmission.js +205 -0
- package/dist/submissions/runSubmission.js.map +1 -0
- package/dist/submissions/types.d.ts +33 -0
- package/dist/submissions/validateSubmission.js +59 -0
- package/dist/submissions/validateSubmission.js.map +1 -0
- package/dist/translations/en.d.ts +12 -0
- package/dist/{translations-edMqq_2e.js → translations/en.js} +3 -22
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/index.d.ts +14 -0
- package/dist/translations/index.js +26 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +132 -0
- package/dist/{keys-N5xGiUsh.js → translations/keys.js} +2 -2
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/makeTranslate.d.ts +11 -0
- package/dist/translations/makeTranslate.js +11 -0
- package/dist/translations/makeTranslate.js.map +1 -0
- package/dist/translations/server.js +25 -0
- package/dist/translations/server.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/dist/uploads/captureFileRef.d.ts +27 -0
- package/dist/uploads/captureFileRef.js +34 -0
- package/dist/uploads/captureFileRef.js.map +1 -0
- package/dist/uploads/resolveFileRef.d.ts +26 -0
- package/dist/uploads/resolveFileRef.js +44 -0
- package/dist/uploads/resolveFileRef.js.map +1 -0
- package/dist/uploads/types.d.ts +20 -0
- package/dist/validation/buildRuleBlocks.js +46 -0
- package/dist/validation/buildRuleBlocks.js.map +1 -0
- package/dist/validation/builtin/email.js +19 -0
- package/dist/validation/builtin/email.js.map +1 -0
- package/dist/validation/builtin/index.js +27 -0
- package/dist/validation/builtin/index.js.map +1 -0
- package/dist/validation/builtin/matchesField.js +20 -0
- package/dist/validation/builtin/matchesField.js.map +1 -0
- package/dist/validation/builtin/max.js +21 -0
- package/dist/validation/builtin/max.js.map +1 -0
- package/dist/validation/builtin/maxLength.js +26 -0
- package/dist/validation/builtin/maxLength.js.map +1 -0
- package/dist/validation/builtin/min.js +21 -0
- package/dist/validation/builtin/min.js.map +1 -0
- package/dist/validation/builtin/minLength.js +26 -0
- package/dist/validation/builtin/minLength.js.map +1 -0
- package/dist/validation/builtin/notAlreadySubmitted.js +32 -0
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -0
- package/dist/validation/builtin/oneOf.js +34 -0
- package/dist/validation/builtin/oneOf.js.map +1 -0
- package/dist/validation/builtin/pattern.js +41 -0
- package/dist/validation/builtin/pattern.js.map +1 -0
- package/dist/validation/builtin/url.js +22 -0
- package/dist/validation/builtin/url.js.map +1 -0
- package/dist/validation/defineValidationRule.d.ts +12 -0
- package/dist/validation/defineValidationRule.js +11 -0
- package/dist/validation/defineValidationRule.js.map +1 -0
- package/dist/validation/message.js +14 -0
- package/dist/validation/message.js.map +1 -0
- package/dist/validation/registry.d.ts +10 -0
- package/dist/validation/registry.js +20 -0
- package/dist/validation/registry.js.map +1 -0
- package/dist/validation/runValidation.js +111 -0
- package/dist/validation/runValidation.js.map +1 -0
- package/dist/validation/types.d.ts +76 -0
- package/package.json +8 -5
- package/styles/form-builder.css +201 -11
- package/dist/constants-B-BUfetP.d.ts +0 -288
- package/dist/exports/client.js.map +0 -1
- package/dist/exports/react.js.map +0 -1
- package/dist/exports/rsc.js.map +0 -1
- package/dist/fieldTypes-B8jkNRob.d.ts +0 -188
- package/dist/fieldTypes-CzhhJXjg.js.map +0 -1
- package/dist/index-DfFYGbVF.d.ts +0 -481
- package/dist/keys-N5xGiUsh.js.map +0 -1
- package/dist/registry-CoCyhtvB.js +0 -282
- package/dist/registry-CoCyhtvB.js.map +0 -1
- package/dist/resolver-OeQyVH2N.js +0 -665
- package/dist/resolver-OeQyVH2N.js.map +0 -1
- package/dist/translations-edMqq_2e.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Form.js","names":[],"sources":["../../src/react/Form.tsx"],"sourcesContent":["'use client'\n\nimport {\n\tcreateElement,\n\ttype FormEvent as ReactFormEvent,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseId,\n\tuseMemo,\n\tuseReducer,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport { noopEventSink } from '../events/noopSink'\nimport type { FormEventSink } from '../events/types'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from '../flow/engine'\nimport type { FormFlow } from '../flow/types'\nimport { defaultPresentationDescriptors } from '../presentations/defaults'\nimport { interpolate } from '../recall/interpolate'\nimport { buildRecallResolver } from '../recall/resolver'\nimport { CAPTCHA_TOKEN_KEY, DEFAULT_HONEYPOT_FIELD } from '../spam/constants'\nimport type { FormFieldInstance, SubmissionValue } from '../submissions/types'\nimport { en } from '../translations/en'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport type { AnyValidationRuleDefinition } from '../validation/types'\nimport { cn } from './cn'\nimport type { FieldRenderer, RendererTranslate } from './contract'\nimport { emitFormEvent } from './events'\nimport { FormContext, type FormStepInfo } from './FormContext'\nimport { type FieldWidth, FormLayout, widthProps } from './FormLayout'\nimport { Honeypot } from './Honeypot'\nimport { defaultPresentations } from './presentation/presentations'\nimport { type PresentationsConfig, resolvePresentations } from './presentation/registry'\nimport type { FormPresentation } from './presentation/types'\nimport { applyRecall } from './recall'\nimport { type RenderersConfig, resolveRenderers } from './registry'\nimport { defaultRenderers } from './renderers'\nimport { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from './resolveForm'\nimport { type FieldErrors, type FormAction, formReducer, initialFormState } from './state'\nimport { type SubmitFormResult, type SubmitHandler, submitForm } from './submitForm'\nimport { useField } from './useField'\nimport { validateFieldValue } from './validateField'\n\nexport type FormDocument = {\n\tid: number | string\n\tfields: FormFieldInstance[]\n\tflow?: FormFlow\n\t/** Stored presentation name; overridden by the `presentation` prop. */\n\tdefaultPresentation?: string\n}\n\nexport type FormProps = {\n\tform: FormDocument\n\tfieldTypes?: AnyFormFieldDefinition[]\n\trules?: AnyValidationRuleDefinition[]\n\trenderers?: RenderersConfig\n\tapiRoute?: string\n\tonSubmit?: SubmitHandler\n\tonSuccess?: (submissionId?: string) => void\n\tonError?: (message: string) => void\n\tevents?: FormEventSink\n\tt?: RendererTranslate\n\tlocale?: string\n\tlayout?: boolean\n\tsubmitLabel?: string\n\tnextLabel?: string\n\tbackLabel?: string\n\t/** Label for the overlay close control (modal/drawer). */\n\tcloseLabel?: string\n\tsuccessMessage?: string\n\t/** Active presentation: a name into the registry or an inline presentation. Overrides the form's stored default. */\n\tpresentation?: string | FormPresentation\n\t/** Per-render presentation overrides merged onto the defaults (add, replace, or `false` to remove). */\n\tpresentations?: PresentationsConfig\n\t/** Invoked when an overlay presentation dismisses (close button, Escape, outside click, or `dismissOnSuccess`). */\n\tonClose?: () => void\n\t/** Accessible name for an overlay surface. */\n\ttitle?: string\n\t/** Seed initial field values (e.g. from `valuesFromSearchParams`). Still validated on submit. */\n\tinitialValues?: Record<string, unknown>\n\t/** Honeypot decoy (on by default). `false` removes it; `{ name }` matches a customized server `spam.honeypot.fieldName`. */\n\thoneypot?: false | { name?: string }\n\t/** A token from your captcha widget; verified server-side when a captcha provider is configured. */\n\tcaptchaToken?: string\n\t/** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */\n\tchildren?: ReactNode\n\t/** Additional CSS class names applied to the root `<form>` element (and the success node). */\n\tclassName?: string\n}\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\nconst FIELD_WIDTHS = new Set<string>(['full', 'half', 'third', 'twoThirds'])\n\ntype FieldHostProps = {\n\tfield: FormFieldInstance\n\trenderer: FieldRenderer\n\tlocale: string\n\tt: RendererTranslate\n}\n\nconst FieldHost = ({ field, renderer, locale, t }: FieldHostProps) => {\n\tconst id = useId()\n\tconst { value, errors, warnings, setValue, onBlur } = useField(field.name)\n\treturn createElement(renderer, {\n\t\tfield,\n\t\tid,\n\t\tname: field.name,\n\t\tvalue,\n\t\tonChange: setValue,\n\t\tonBlur,\n\t\terrors,\n\t\twarnings,\n\t\trequired: Boolean(field.required),\n\t\tlocale,\n\t\tt,\n\t})\n}\n\ntype CalcFieldHostProps = FieldHostProps & { value: unknown }\n\n/** Hosts a derived (calc) field: read-only, value supplied from `effectiveValues`, never bound via `useField`. */\nconst CalcFieldHost = ({ field, renderer, value, locale, t }: CalcFieldHostProps) => {\n\tconst id = useId()\n\treturn createElement(renderer, {\n\t\tfield,\n\t\tid,\n\t\tname: field.name,\n\t\tvalue,\n\t\tonChange: () => {},\n\t\tonBlur: () => {},\n\t\terrors: [],\n\t\twarnings: [],\n\t\trequired: false,\n\t\tdisabled: true,\n\t\tlocale,\n\t\tt,\n\t})\n}\n\n/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */\nexport const Form = ({\n\tform,\n\tfieldTypes,\n\trules,\n\trenderers,\n\tapiRoute,\n\tonSubmit,\n\tonSuccess,\n\tonError,\n\tevents,\n\tt,\n\tlocale = 'en',\n\tlayout,\n\tsubmitLabel = 'Submit',\n\tnextLabel = 'Next',\n\tbackLabel = 'Back',\n\tcloseLabel = 'Close',\n\tsuccessMessage = 'Thank you.',\n\tpresentation,\n\tpresentations,\n\tonClose,\n\ttitle,\n\tinitialValues,\n\thoneypot,\n\tcaptchaToken,\n\tchildren,\n\tclassName,\n}: FormProps) => {\n\tconst honeypotName = honeypot === false ? null : (honeypot?.name ?? DEFAULT_HONEYPOT_FIELD)\n\tconst honeypotRef = useRef<HTMLInputElement>(null)\n\tconst registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes])\n\tconst ruleRegistry = useMemo(() => buildValidationRuleRegistry(rules), [rules])\n\tconst rendererRegistry = useMemo(() => resolveRenderers(defaultRenderers, renderers), [renderers])\n\tconst presentationRegistry = useMemo(\n\t\t() => resolvePresentations(defaultPresentations, presentations),\n\t\t[presentations]\n\t)\n\tconst activePresentation: FormPresentation =\n\t\ttypeof presentation === 'object'\n\t\t\t? presentation\n\t\t\t: (presentationRegistry.get(presentation ?? form.defaultPresentation ?? 'page') ??\n\t\t\t\tpresentationRegistry.get('page') ??\n\t\t\t\tdefaultPresentationDescriptors.page)\n\tconst fieldsByName = useMemo(\n\t\t() => new Map(form.fields.map((field) => [field.name, field])),\n\t\t[form.fields]\n\t)\n\tconst translate = useMemo<RendererTranslate>(() => t ?? makeTranslate(en), [t])\n\n\t// Latest-value refs so event emission and the mount/unmount effect tolerate an inline `events` prop or a changing form id.\n\tconst sinkRef = useRef<FormEventSink>(noopEventSink)\n\tsinkRef.current = events ?? noopEventSink\n\tconst formIdRef = useRef('')\n\tformIdRef.current = String(form.id)\n\n\tconst [state, rawDispatch] = useReducer(formReducer, form.fields, (fields) =>\n\t\tinitialFormState({\n\t\t\t...Object.fromEntries(fields.map((field) => [field.name, undefined])),\n\t\t\t...(initialValues ?? {}),\n\t\t})\n\t)\n\n\t// Authoritative values for derived (calc) fields, recomputed from user answers on every change. The\n\t// server recomputes these too at submit; the client copy drives the live calc renderer, recall, and submit.\n\tconst effectiveValues = useMemo(\n\t\t() => computeCalcFields(form.fields, state.values),\n\t\t[form.fields, state.values]\n\t)\n\n\tconst recall = useMemo(\n\t\t() =>\n\t\t\tbuildRecallResolver({\n\t\t\t\tfields: form.fields,\n\t\t\t\tvalues: effectiveValues,\n\t\t\t\tregistry,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}),\n\t\t[form.fields, effectiveValues, registry, locale, translate]\n\t)\n\n\t// Multi-step is active only when a flow declares two or more steps; otherwise this is an ordinary single-step form.\n\tconst flow = form.flow && form.flow.steps.length >= 2 ? form.flow : undefined\n\tconst [currentStepId, setCurrentStepId] = useState<string | undefined>(() =>\n\t\tflow ? firstStepId(flow) : undefined\n\t)\n\tconst [history, setHistory] = useState<string[]>([])\n\n\tconst startedRef = useRef(false)\n\tconst submittedRef = useRef(false)\n\tconst submittingRef = useRef(false)\n\tconst flowRef = useRef(flow)\n\tflowRef.current = flow\n\n\tconst dispatch = useCallback((action: FormAction) => {\n\t\tif (action.type === 'SET_VALUE' && !startedRef.current) {\n\t\t\tstartedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.started' })\n\t\t}\n\t\trawDispatch(action)\n\t}, [])\n\n\tconst validateField = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tconst field = fieldsByName.get(name)\n\t\t\tif (!field) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst answers = { ...effectiveValues, [name]: value }\n\t\t\t// Mirror the server: a field whose `validateWhen` is unmet is not validated; clear any stale error.\n\t\t\tif (!evaluateCondition(field.validateWhen, answers)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors: [], warnings: [] })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvoid validateFieldValue({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}).then(({ errors, warnings }) => {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors, warnings })\n\t\t\t\tconst [firstError] = errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[fieldsByName, effectiveValues, registry, ruleRegistry, locale, translate]\n\t)\n\n\tconst visible = visibleFields(form.fields, effectiveValues)\n\tconst stepNames = flow && currentStepId ? stepFieldNames(flow, currentStepId) : []\n\tconst stepVisible: FormFieldInstance[] = stepNames\n\t\t.map((name) => visible.find((field) => field.name === name))\n\t\t.filter((field): field is FormFieldInstance => Boolean(field))\n\n\tconst goNext = async () => {\n\t\tif (!flow || !currentStepId) {\n\t\t\treturn\n\t\t}\n\t\tconst results = await Promise.all(\n\t\t\tstepVisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tlet hasError = false\n\t\tfor (const result of results) {\n\t\t\trawDispatch({ type: 'TOUCH', name: result.field.name })\n\t\t\trawDispatch({\n\t\t\t\ttype: 'SET_FIELD_ISSUES',\n\t\t\t\tname: result.field.name,\n\t\t\t\terrors: result.errors,\n\t\t\t\twarnings: result.warnings,\n\t\t\t})\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\thasError = true\n\t\t\t}\n\t\t}\n\t\tif (hasError) {\n\t\t\treturn\n\t\t}\n\t\tconst next = resolveNextStepId(flow, currentStepId, state.values)\n\t\tif (!next) {\n\t\t\treturn\n\t\t}\n\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\ttype: 'step.completed',\n\t\t\tstepId: currentStepId,\n\t\t})\n\t\tsetHistory((prev) => [...prev, currentStepId])\n\t\tsetCurrentStepId(next)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: next })\n\t}\n\n\tconst goBack = () => {\n\t\tconst prev = history[history.length - 1]\n\t\tif (prev === undefined) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory((entries) => entries.slice(0, -1))\n\t\tsetCurrentStepId(prev)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: prev })\n\t}\n\n\tuseEffect(() => {\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.viewed' })\n\t\tconst mountFlow = flowRef.current\n\t\tif (mountFlow) {\n\t\t\tconst first = firstStepId(mountFlow)\n\t\t\tif (first) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: first })\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!submittedRef.current && !submittingRef.current) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.abandoned' })\n\t\t\t}\n\t\t}\n\t}, [])\n\n\tconst handleClose = useCallback(() => {\n\t\tonClose?.()\n\t}, [onClose])\n\n\tconst handleSubmit = async (event: ReactFormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault()\n\t\t// Re-entrancy guard: claim the in-flight slot before the async validation window so a fast second\n\t\t// activation (double-click, Enter + click) cannot reach the transport and POST the submission twice.\n\t\tif (submittingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tsubmittingRef.current = true\n\t\tconst visible = visibleFields(form.fields, effectiveValues)\n\t\tconst results = await Promise.all(\n\t\t\t// Calc fields carry no rules and have no input; they are always satisfied, so skip validating them.\n\t\t\t// A field whose `validateWhen` is unmet is skipped too, mirroring the server (no client/server divergence).\n\t\t\tvisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tconst errors: FieldErrors = {}\n\t\tconst warnings: FieldErrors = {}\n\t\tfor (const result of results) {\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\terrors[result.field.name] = result.errors\n\t\t\t\tconst [firstError] = result.errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: result.field.name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (result.warnings.length > 0) {\n\t\t\t\twarnings[result.field.name] = result.warnings\n\t\t\t}\n\t\t}\n\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors, warnings })\n\t\tif (Object.keys(errors).length > 0) {\n\t\t\tsubmittingRef.current = false\n\t\t\treturn\n\t\t}\n\t\trawDispatch({ type: 'SUBMIT_START' })\n\t\tconst values: SubmissionValue[] = visible\n\t\t\t.filter((field) => !isEmpty(effectiveValues[field.name]))\n\t\t\t.map((field) => ({ field: field.name, value: effectiveValues[field.name] }))\n\t\tif (honeypotName) {\n\t\t\tconst decoy = honeypotRef.current?.value ?? ''\n\t\t\tif (decoy !== '') {\n\t\t\t\tvalues.push({ field: honeypotName, value: decoy })\n\t\t\t}\n\t\t}\n\t\tif (captchaToken) {\n\t\t\tvalues.push({ field: CAPTCHA_TOKEN_KEY, value: captchaToken })\n\t\t}\n\t\tconst result: SubmitFormResult = onSubmit\n\t\t\t? await onSubmit({ formId: form.id, values })\n\t\t\t: await submitForm({ formId: form.id, values, apiRoute })\n\t\tsubmittingRef.current = false\n\t\tif (result.ok) {\n\t\t\tsubmittedRef.current = true\n\t\t\trawDispatch({ type: 'SUBMIT_SUCCESS' })\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'submission.created',\n\t\t\t\tsubmissionId: result.submissionId,\n\t\t\t})\n\t\t\tonSuccess?.(result.submissionId)\n\t\t\tif (activePresentation.dismissOnSuccess) {\n\t\t\t\thandleClose()\n\t\t\t}\n\t\t} else {\n\t\t\tif (result.fieldErrors) {\n\t\t\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors: result.fieldErrors, warnings: {} })\n\t\t\t}\n\t\t\tconst message = result.message ?? 'Submission failed'\n\t\t\trawDispatch({ type: 'SUBMIT_ERROR', message })\n\t\t\tonError?.(message)\n\t\t}\n\t}\n\n\tconst step: FormStepInfo = flow\n\t\t? {\n\t\t\t\tflow,\n\t\t\t\tcurrentStepId,\n\t\t\t\tstepIndex: flow.steps.findIndex((s) => s.id === currentStepId),\n\t\t\t\tstepCount: flow.steps.length,\n\t\t\t\tisFirst: history.length === 0,\n\t\t\t\tisTerminal: currentStepId ? isTerminalStepId(flow, currentStepId, state.values) : true,\n\t\t\t\tgoNext: () => {\n\t\t\t\t\tvoid goNext()\n\t\t\t\t},\n\t\t\t\tgoBack,\n\t\t\t}\n\t\t: {\n\t\t\t\tstepIndex: 0,\n\t\t\t\tstepCount: 1,\n\t\t\t\tisFirst: true,\n\t\t\t\tisTerminal: true,\n\t\t\t\tgoNext: () => {},\n\t\t\t\tgoBack: () => {},\n\t\t\t}\n\n\tconst contextValue = { state, dispatch, validateField, locale, step }\n\n\tconst PresentationWrapper = activePresentation.Wrapper\n\tconst wrap = (content: ReactNode): ReactNode =>\n\t\tPresentationWrapper ? (\n\t\t\t<PresentationWrapper\n\t\t\t\tpresentation={activePresentation}\n\t\t\t\topen\n\t\t\t\tonClose={handleClose}\n\t\t\t\ttitle={title}\n\t\t\t\tcloseLabel={closeLabel}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</PresentationWrapper>\n\t\t) : (\n\t\t\tcontent\n\t\t)\n\n\tif (children !== undefined) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<form\n\t\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\t\tnoValidate\n\t\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tif (state.submitted) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<p\n\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{interpolate(successMessage, recall)}\n\t\t\t\t\t</p>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tconst rendered = (flow ? stepVisible : visible).filter(\n\t\t(field) => field.hidden !== true && field.calcDisplay !== false\n\t)\n\n\treturn (\n\t\t<FormContext.Provider value={contextValue}>\n\t\t\t{wrap(\n\t\t\t\t<form\n\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\tnoValidate\n\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t>\n\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t<FormLayout enabled={layout !== false}>\n\t\t\t\t\t\t{rendered.map((field) => {\n\t\t\t\t\t\t\tconst renderer = rendererRegistry.get(field.blockType)\n\t\t\t\t\t\t\tif (!renderer) {\n\t\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst width: FieldWidth | undefined =\n\t\t\t\t\t\t\t\ttypeof field.width === 'string' && FIELD_WIDTHS.has(field.width)\n\t\t\t\t\t\t\t\t\t? (field.width as FieldWidth)\n\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\tconst recalledField = applyRecall(field, recall)\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={field.name} {...widthProps(width)}>\n\t\t\t\t\t\t\t\t\t{calcExpressionOf(field) ? (\n\t\t\t\t\t\t\t\t\t\t<CalcFieldHost\n\t\t\t\t\t\t\t\t\t\t\tfield={recalledField}\n\t\t\t\t\t\t\t\t\t\t\trenderer={renderer}\n\t\t\t\t\t\t\t\t\t\t\tvalue={effectiveValues[field.name]}\n\t\t\t\t\t\t\t\t\t\t\tlocale={locale}\n\t\t\t\t\t\t\t\t\t\t\tt={translate}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<FieldHost\n\t\t\t\t\t\t\t\t\t\t\tfield={recalledField}\n\t\t\t\t\t\t\t\t\t\t\trenderer={renderer}\n\t\t\t\t\t\t\t\t\t\t\tlocale={locale}\n\t\t\t\t\t\t\t\t\t\t\tt={translate}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t</FormLayout>\n\t\t\t\t\t{state.submitError ? (\n\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__submit-error\">\n\t\t\t\t\t\t\t{state.submitError}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t) : null}\n\t\t\t\t\t{flow ? (\n\t\t\t\t\t\t<div className=\"fb-form__controls\">\n\t\t\t\t\t\t\t{!step.isFirst ? (\n\t\t\t\t\t\t\t\t<button type=\"button\" onClick={goBack} disabled={state.submitting}>\n\t\t\t\t\t\t\t\t\t{backLabel}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t{step.isTerminal ? (\n\t\t\t\t\t\t\t\t<button type=\"submit\" disabled={state.submitting}>\n\t\t\t\t\t\t\t\t\t{submitLabel}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tdisabled={state.submitting}\n\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\tvoid goNext()\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{nextLabel}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<button type=\"submit\" disabled={state.submitting}>\n\t\t\t\t\t\t\t{submitLabel}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t)}\n\t\t\t\t</form>\n\t\t\t)}\n\t\t</FormContext.Provider>\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FormFlow } from "../flow/types.js";
|
|
2
|
+
//#region src/react/FormContext.d.ts
|
|
3
|
+
/** Multi-step navigation state. Defaults to a single terminal step when the form has no flow. */
|
|
4
|
+
type FormStepInfo = {
|
|
5
|
+
flow?: FormFlow;
|
|
6
|
+
currentStepId?: string;
|
|
7
|
+
stepIndex: number;
|
|
8
|
+
stepCount: number;
|
|
9
|
+
isFirst: boolean;
|
|
10
|
+
isTerminal: boolean;
|
|
11
|
+
goNext: () => void;
|
|
12
|
+
goBack: () => void;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { FormStepInfo };
|
|
16
|
+
//# sourceMappingURL=FormContext.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
//#region src/react/FormContext.ts
|
|
4
|
+
const FormContext = createContext(null);
|
|
5
|
+
/** Read the form controller context. Throws if used outside `<Form>`. */
|
|
6
|
+
const useFormContext = () => {
|
|
7
|
+
const context = useContext(FormContext);
|
|
8
|
+
if (!context) throw new Error("useFormContext must be used within a <Form>");
|
|
9
|
+
return context;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { FormContext, useFormContext };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=FormContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormContext.js","names":[],"sources":["../../src/react/FormContext.ts"],"sourcesContent":["'use client'\n\nimport type { Dispatch } from 'react'\nimport { createContext, useContext } from 'react'\nimport type { FormFlow } from '../flow/types'\nimport type { FormAction, FormState } from './state'\n\n/** Multi-step navigation state. Defaults to a single terminal step when the form has no flow. */\nexport type FormStepInfo = {\n\tflow?: FormFlow\n\tcurrentStepId?: string\n\tstepIndex: number\n\tstepCount: number\n\tisFirst: boolean\n\tisTerminal: boolean\n\tgoNext: () => void\n\tgoBack: () => void\n}\n\nexport type FormContextValue = {\n\tstate: FormState\n\tdispatch: Dispatch<FormAction>\n\t/** Validate one field now (client mode) against the supplied value and store its issues. */\n\tvalidateField: (name: string, value: unknown) => void\n\tlocale: string\n\tstep: FormStepInfo\n}\n\nexport const FormContext = createContext<FormContextValue | null>(null)\n\n/** Read the form controller context. Throws if used outside `<Form>`. */\nexport const useFormContext = (): FormContextValue => {\n\tconst context = useContext(FormContext)\n\tif (!context) {\n\t\tthrow new Error('useFormContext must be used within a <Form>')\n\t}\n\treturn context\n}\n"],"mappings":";;;AA4BA,MAAa,cAAc,cAAuC,IAAI;;AAGtE,MAAa,uBAAyC;CACrD,MAAM,UAAU,WAAW,WAAW;CACtC,IAAI,CAAC,SACJ,MAAM,IAAI,MAAM,6CAA6C;CAE9D,OAAO;AACR"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/react/FormLayout.d.ts
|
|
4
|
+
type FieldWidth = 'full' | 'half' | 'third' | 'twoThirds';
|
|
5
|
+
type FormLayoutProps = {
|
|
6
|
+
children: ReactNode; /** When false, render a plain container with no grid class (document-order layout). */
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** Establishes the layout container. With the stylesheet imported, children with `data-width` flow into a grid. */
|
|
10
|
+
declare const FormLayout: ({
|
|
11
|
+
children,
|
|
12
|
+
enabled
|
|
13
|
+
}: FormLayoutProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
/** Props to spread onto a field wrapper to declare its grid width. */
|
|
15
|
+
declare const widthProps: (width: FieldWidth | undefined) => {
|
|
16
|
+
'data-width': FieldWidth;
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { FieldWidth, FormLayout, FormLayoutProps, widthProps };
|
|
20
|
+
//# sourceMappingURL=FormLayout.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/react/FormLayout.tsx
|
|
4
|
+
/** Establishes the layout container. With the stylesheet imported, children with `data-width` flow into a grid. */
|
|
5
|
+
const FormLayout = ({ children, enabled = true }) => /* @__PURE__ */ jsx("div", {
|
|
6
|
+
className: enabled ? "fb-form fb-form--grid" : "fb-form",
|
|
7
|
+
children
|
|
8
|
+
});
|
|
9
|
+
/** Props to spread onto a field wrapper to declare its grid width. */
|
|
10
|
+
const widthProps = (width) => ({ "data-width": width ?? "full" });
|
|
11
|
+
//#endregion
|
|
12
|
+
export { FormLayout, widthProps };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=FormLayout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormLayout.js","names":[],"sources":["../../src/react/FormLayout.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\n\nexport type FieldWidth = 'full' | 'half' | 'third' | 'twoThirds'\n\nexport type FormLayoutProps = {\n\tchildren: ReactNode\n\t/** When false, render a plain container with no grid class (document-order layout). */\n\tenabled?: boolean\n}\n\n/** Establishes the layout container. With the stylesheet imported, children with `data-width` flow into a grid. */\nexport const FormLayout = ({ children, enabled = true }: FormLayoutProps) => (\n\t<div className={enabled ? 'fb-form fb-form--grid' : 'fb-form'}>{children}</div>\n)\n\n/** Props to spread onto a field wrapper to declare its grid width. */\nexport const widthProps = (width: FieldWidth | undefined) => ({ 'data-width': width ?? 'full' })\n"],"mappings":";;;;AAaA,MAAa,cAAc,EAAE,UAAU,UAAU,WAChD,oBAAC,OAAD;CAAK,WAAW,UAAU,0BAA0B;CAAY;AAAc,CAAA;;AAI/E,MAAa,cAAc,WAAmC,EAAE,cAAc,SAAS,OAAO"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FieldAggregation } from "../aggregation/types.js";
|
|
2
|
+
import { RendererTranslate } from "./contract.js";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/react/FormResults.d.ts
|
|
6
|
+
type FormResultsProps = {
|
|
7
|
+
/** One field aggregation or several (a survey summary), resolved server-side and passed in. */results: FieldAggregation | FieldAggregation[]; /** Localized translator for built-in copy. Defaults to identity (returns the key). */
|
|
8
|
+
t?: RendererTranslate;
|
|
9
|
+
locale?: string; /** Show the raw count beside each option. Default true. */
|
|
10
|
+
showCounts?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Presentational results view for polls and survey summaries: per-option bars with percentages. Headless,
|
|
14
|
+
* data resolved server-side via `aggregateFieldResponses`/`aggregateFormResponses` and passed in (it never
|
|
15
|
+
* fetches). The option label, count, and percentage are real text (the accessible content); the bar fill
|
|
16
|
+
* is `aria-hidden` visual sugar sized by inline width.
|
|
17
|
+
*/
|
|
18
|
+
declare const FormResults: ({
|
|
19
|
+
results,
|
|
20
|
+
t,
|
|
21
|
+
showCounts
|
|
22
|
+
}: FormResultsProps) => ReactNode;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { FormResults, FormResultsProps };
|
|
25
|
+
//# sourceMappingURL=FormResults.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { keys } from "../translations/keys.js";
|
|
3
|
+
import { en } from "../translations/en.js";
|
|
4
|
+
import { makeTranslate } from "../translations/makeTranslate.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/react/FormResults.tsx
|
|
7
|
+
const OneResult = ({ result, t, showCounts }) => {
|
|
8
|
+
if (result.total === 0) return /* @__PURE__ */ jsxs("section", {
|
|
9
|
+
className: "fb-results__field",
|
|
10
|
+
children: [result.label ? /* @__PURE__ */ jsx("h3", {
|
|
11
|
+
className: "fb-results__label",
|
|
12
|
+
children: result.label
|
|
13
|
+
}) : null, /* @__PURE__ */ jsx("p", {
|
|
14
|
+
className: "fb-results__empty",
|
|
15
|
+
children: t(keys.resultsNoResponses)
|
|
16
|
+
})]
|
|
17
|
+
});
|
|
18
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
19
|
+
className: "fb-results__field",
|
|
20
|
+
children: [
|
|
21
|
+
result.label ? /* @__PURE__ */ jsx("h3", {
|
|
22
|
+
className: "fb-results__label",
|
|
23
|
+
children: result.label
|
|
24
|
+
}) : null,
|
|
25
|
+
/* @__PURE__ */ jsx("ul", {
|
|
26
|
+
className: "fb-results__list",
|
|
27
|
+
children: result.buckets.map((bucket) => /* @__PURE__ */ jsxs("li", {
|
|
28
|
+
className: "fb-results__row",
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsx("span", {
|
|
31
|
+
className: "fb-results__option",
|
|
32
|
+
children: bucket.label
|
|
33
|
+
}),
|
|
34
|
+
/* @__PURE__ */ jsx("span", {
|
|
35
|
+
className: "fb-results__bar",
|
|
36
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
37
|
+
className: "fb-results__bar-fill",
|
|
38
|
+
style: { width: `${bucket.percentage}%` },
|
|
39
|
+
"aria-hidden": "true"
|
|
40
|
+
})
|
|
41
|
+
}),
|
|
42
|
+
/* @__PURE__ */ jsxs("span", {
|
|
43
|
+
className: "fb-results__pct",
|
|
44
|
+
children: [bucket.percentage, "%"]
|
|
45
|
+
}),
|
|
46
|
+
showCounts ? /* @__PURE__ */ jsx("span", {
|
|
47
|
+
className: "fb-results__count",
|
|
48
|
+
children: bucket.count
|
|
49
|
+
}) : null
|
|
50
|
+
]
|
|
51
|
+
}, bucket.value))
|
|
52
|
+
}),
|
|
53
|
+
/* @__PURE__ */ jsxs("p", {
|
|
54
|
+
className: "fb-results__total",
|
|
55
|
+
children: [
|
|
56
|
+
result.total,
|
|
57
|
+
" ",
|
|
58
|
+
t(keys.resultsResponses),
|
|
59
|
+
result.truncated ? ` (${t(keys.resultsTruncated)})` : ""
|
|
60
|
+
]
|
|
61
|
+
})
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Presentational results view for polls and survey summaries: per-option bars with percentages. Headless,
|
|
67
|
+
* data resolved server-side via `aggregateFieldResponses`/`aggregateFormResponses` and passed in (it never
|
|
68
|
+
* fetches). The option label, count, and percentage are real text (the accessible content); the bar fill
|
|
69
|
+
* is `aria-hidden` visual sugar sized by inline width.
|
|
70
|
+
*/
|
|
71
|
+
const FormResults = ({ results, t, showCounts = true }) => {
|
|
72
|
+
const translate = t ?? makeTranslate(en);
|
|
73
|
+
return /* @__PURE__ */ jsx("div", {
|
|
74
|
+
className: "fb-results",
|
|
75
|
+
children: (Array.isArray(results) ? results : [results]).map((result) => /* @__PURE__ */ jsx(OneResult, {
|
|
76
|
+
result,
|
|
77
|
+
t: translate,
|
|
78
|
+
showCounts
|
|
79
|
+
}, result.field))
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { FormResults };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=FormResults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormResults.js","names":[],"sources":["../../src/react/FormResults.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport type { RendererTranslate } from './contract'\n\nexport type FormResultsProps = {\n\t/** One field aggregation or several (a survey summary), resolved server-side and passed in. */\n\tresults: FieldAggregation | FieldAggregation[]\n\t/** Localized translator for built-in copy. Defaults to identity (returns the key). */\n\tt?: RendererTranslate\n\tlocale?: string\n\t/** Show the raw count beside each option. Default true. */\n\tshowCounts?: boolean\n}\n\nconst OneResult = ({\n\tresult,\n\tt,\n\tshowCounts,\n}: {\n\tresult: FieldAggregation\n\tt: RendererTranslate\n\tshowCounts: boolean\n}): ReactNode => {\n\tif (result.total === 0) {\n\t\treturn (\n\t\t\t<section className=\"fb-results__field\">\n\t\t\t\t{result.label ? <h3 className=\"fb-results__label\">{result.label}</h3> : null}\n\t\t\t\t<p className=\"fb-results__empty\">{t(keys.resultsNoResponses)}</p>\n\t\t\t</section>\n\t\t)\n\t}\n\treturn (\n\t\t<section className=\"fb-results__field\">\n\t\t\t{result.label ? <h3 className=\"fb-results__label\">{result.label}</h3> : null}\n\t\t\t<ul className=\"fb-results__list\">\n\t\t\t\t{result.buckets.map((bucket) => (\n\t\t\t\t\t<li key={bucket.value} className=\"fb-results__row\">\n\t\t\t\t\t\t<span className=\"fb-results__option\">{bucket.label}</span>\n\t\t\t\t\t\t<span className=\"fb-results__bar\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"fb-results__bar-fill\"\n\t\t\t\t\t\t\t\tstyle={{ width: `${bucket.percentage}%` }}\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span className=\"fb-results__pct\">{bucket.percentage}%</span>\n\t\t\t\t\t\t{showCounts ? <span className=\"fb-results__count\">{bucket.count}</span> : null}\n\t\t\t\t\t</li>\n\t\t\t\t))}\n\t\t\t</ul>\n\t\t\t<p className=\"fb-results__total\">\n\t\t\t\t{result.total} {t(keys.resultsResponses)}\n\t\t\t\t{result.truncated ? ` (${t(keys.resultsTruncated)})` : ''}\n\t\t\t</p>\n\t\t</section>\n\t)\n}\n\n/**\n * Presentational results view for polls and survey summaries: per-option bars with percentages. Headless,\n * data resolved server-side via `aggregateFieldResponses`/`aggregateFormResponses` and passed in (it never\n * fetches). The option label, count, and percentage are real text (the accessible content); the bar fill\n * is `aria-hidden` visual sugar sized by inline width.\n */\nexport const FormResults = ({ results, t, showCounts = true }: FormResultsProps): ReactNode => {\n\tconst translate: RendererTranslate = t ?? makeTranslate(en)\n\tconst list = Array.isArray(results) ? results : [results]\n\treturn (\n\t\t<div className=\"fb-results\">\n\t\t\t{list.map((result) => (\n\t\t\t\t<OneResult key={result.field} result={result} t={translate} showCounts={showCounts} />\n\t\t\t))}\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;AAmBA,MAAM,aAAa,EAClB,QACA,GACA,iBAKgB;CAChB,IAAI,OAAO,UAAU,GACpB,OACC,qBAAC,WAAD;EAAS,WAAU;YAAnB,CACE,OAAO,QAAQ,oBAAC,MAAD;GAAI,WAAU;aAAqB,OAAO;EAAU,CAAA,IAAI,MACxE,oBAAC,KAAD;GAAG,WAAU;aAAqB,EAAE,KAAK,kBAAkB;EAAK,CAAA,CACxD;;CAGX,OACC,qBAAC,WAAD;EAAS,WAAU;YAAnB;GACE,OAAO,QAAQ,oBAAC,MAAD;IAAI,WAAU;cAAqB,OAAO;GAAU,CAAA,IAAI;GACxE,oBAAC,MAAD;IAAI,WAAU;cACZ,OAAO,QAAQ,KAAK,WACpB,qBAAC,MAAD;KAAuB,WAAU;eAAjC;MACC,oBAAC,QAAD;OAAM,WAAU;iBAAsB,OAAO;MAAY,CAAA;MACzD,oBAAC,QAAD;OAAM,WAAU;iBACf,oBAAC,QAAD;QACC,WAAU;QACV,OAAO,EAAE,OAAO,GAAG,OAAO,WAAW,GAAG;QACxC,eAAY;OACZ,CAAA;MACI,CAAA;MACN,qBAAC,QAAD;OAAM,WAAU;iBAAhB,CAAmC,OAAO,YAAW,GAAO;;MAC3D,aAAa,oBAAC,QAAD;OAAM,WAAU;iBAAqB,OAAO;MAAY,CAAA,IAAI;KACvE;OAXK,OAAO,KAWZ,CACJ;GACE,CAAA;GACJ,qBAAC,KAAD;IAAG,WAAU;cAAb;KACE,OAAO;KAAM;KAAE,EAAE,KAAK,gBAAgB;KACtC,OAAO,YAAY,KAAK,EAAE,KAAK,gBAAgB,EAAE,KAAK;IACrD;;EACK;;AAEX;;;;;;;AAQA,MAAa,eAAe,EAAE,SAAS,GAAG,aAAa,WAAwC;CAC9F,MAAM,YAA+B,KAAK,cAAc,EAAE;CAE1D,OACC,oBAAC,OAAD;EAAK,WAAU;aAFH,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO,GAGhD,KAAK,WACV,oBAAC,WAAD;GAAsC;GAAQ,GAAG;GAAuB;EAAa,GAArE,OAAO,KAA8D,CACrF;CACG,CAAA;AAEP"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Ref } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/react/Honeypot.d.ts
|
|
4
|
+
type HoneypotProps = {
|
|
5
|
+
name: string;
|
|
6
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A visually hidden honeypot decoy. Real users never see or tab to it; bots that fill every input
|
|
10
|
+
* trip it, and the server rejects the submission.
|
|
11
|
+
*
|
|
12
|
+
* `autoComplete="new-password"` is used instead of `"off"` because Chrome frequently ignores `"off"`
|
|
13
|
+
* but reliably respects `"new-password"` to prevent autofill. The field name is intentionally generic
|
|
14
|
+
* (no "email" token) to avoid Chrome's email-address heuristic.
|
|
15
|
+
*/
|
|
16
|
+
declare const Honeypot: ({
|
|
17
|
+
name,
|
|
18
|
+
inputRef
|
|
19
|
+
}: HoneypotProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { Honeypot, HoneypotProps };
|
|
22
|
+
//# sourceMappingURL=Honeypot.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
//#region src/react/Honeypot.tsx
|
|
4
|
+
const HIDDEN = {
|
|
5
|
+
position: "absolute",
|
|
6
|
+
width: 1,
|
|
7
|
+
height: 1,
|
|
8
|
+
padding: 0,
|
|
9
|
+
margin: -1,
|
|
10
|
+
overflow: "hidden",
|
|
11
|
+
clip: "rect(0, 0, 0, 0)",
|
|
12
|
+
whiteSpace: "nowrap",
|
|
13
|
+
border: 0
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A visually hidden honeypot decoy. Real users never see or tab to it; bots that fill every input
|
|
17
|
+
* trip it, and the server rejects the submission.
|
|
18
|
+
*
|
|
19
|
+
* `autoComplete="new-password"` is used instead of `"off"` because Chrome frequently ignores `"off"`
|
|
20
|
+
* but reliably respects `"new-password"` to prevent autofill. The field name is intentionally generic
|
|
21
|
+
* (no "email" token) to avoid Chrome's email-address heuristic.
|
|
22
|
+
*/
|
|
23
|
+
const Honeypot = ({ name, inputRef }) => /* @__PURE__ */ jsx("div", {
|
|
24
|
+
"aria-hidden": "true",
|
|
25
|
+
style: HIDDEN,
|
|
26
|
+
children: /* @__PURE__ */ jsxs("label", { children: ["Leave this field empty", /* @__PURE__ */ jsx("input", {
|
|
27
|
+
ref: inputRef,
|
|
28
|
+
type: "text",
|
|
29
|
+
name,
|
|
30
|
+
tabIndex: -1,
|
|
31
|
+
autoComplete: "new-password",
|
|
32
|
+
defaultValue: ""
|
|
33
|
+
})] })
|
|
34
|
+
});
|
|
35
|
+
//#endregion
|
|
36
|
+
export { Honeypot };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=Honeypot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Honeypot.js","names":[],"sources":["../../src/react/Honeypot.tsx"],"sourcesContent":["'use client'\n\nimport type { CSSProperties, Ref } from 'react'\n\nconst HIDDEN: CSSProperties = {\n\tposition: 'absolute',\n\twidth: 1,\n\theight: 1,\n\tpadding: 0,\n\tmargin: -1,\n\toverflow: 'hidden',\n\tclip: 'rect(0, 0, 0, 0)',\n\twhiteSpace: 'nowrap',\n\tborder: 0,\n}\n\nexport type HoneypotProps = {\n\tname: string\n\tinputRef?: Ref<HTMLInputElement>\n}\n\n/**\n * A visually hidden honeypot decoy. Real users never see or tab to it; bots that fill every input\n * trip it, and the server rejects the submission.\n *\n * `autoComplete=\"new-password\"` is used instead of `\"off\"` because Chrome frequently ignores `\"off\"`\n * but reliably respects `\"new-password\"` to prevent autofill. The field name is intentionally generic\n * (no \"email\" token) to avoid Chrome's email-address heuristic.\n */\nexport const Honeypot = ({ name, inputRef }: HoneypotProps) => (\n\t<div aria-hidden=\"true\" style={HIDDEN}>\n\t\t<label>\n\t\t\tLeave this field empty\n\t\t\t<input\n\t\t\t\tref={inputRef}\n\t\t\t\ttype=\"text\"\n\t\t\t\tname={name}\n\t\t\t\ttabIndex={-1}\n\t\t\t\tautoComplete=\"new-password\"\n\t\t\t\tdefaultValue=\"\"\n\t\t\t/>\n\t\t</label>\n\t</div>\n)\n"],"mappings":";;;AAIA,MAAM,SAAwB;CAC7B,UAAU;CACV,OAAO;CACP,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,UAAU;CACV,MAAM;CACN,YAAY;CACZ,QAAQ;AACT;;;;;;;;;AAeA,MAAa,YAAY,EAAE,MAAM,eAChC,oBAAC,OAAD;CAAK,eAAY;CAAO,OAAO;WAC9B,qBAAC,SAAD,EAAA,UAAA,CAAO,0BAEN,oBAAC,SAAD;EACC,KAAK;EACL,MAAK;EACC;EACN,UAAU;EACV,cAAa;EACb,cAAa;CACb,CAAA,CACK,EAAA,CAAA;AACH,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FormProps } from "./Form.js";
|
|
2
|
+
import { fetchFormResults } from "./fetchResults.js";
|
|
3
|
+
|
|
4
|
+
//#region src/react/Poll.d.ts
|
|
5
|
+
type PollProps = FormProps & {
|
|
6
|
+
/** The choice field whose results are shown after voting (should match the form's public `resultsField`). */resultsField: string; /** localStorage key for the per-browser voted guard. Default `fb-poll-{form.id}`. */
|
|
7
|
+
storageKey?: string; /** Force the voted state (controlled). Omit to use the localStorage guard. */
|
|
8
|
+
hasVoted?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module
|
|
11
|
+
* scope, `useCallback`, or `useMemo`): an inline function re-runs the load effect and double-fetches.
|
|
12
|
+
*/
|
|
13
|
+
fetchResultsImpl?: typeof fetchFormResults;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A poll: renders `<Form>` until the visitor votes, then fetches the aggregate results and shows
|
|
17
|
+
* `<FormResults>`. A per-browser localStorage flag (`storageKey`) skips straight to results on revisit. The
|
|
18
|
+
* guard is UX, not integrity (bypassable): server-enforced one-per-identity dedup composes via `req.user`
|
|
19
|
+
* (authed forms) or a `notAlreadySubmitted` rule, with cookie/IP identity deferred to the spam phase.
|
|
20
|
+
*/
|
|
21
|
+
declare const Poll: ({
|
|
22
|
+
resultsField,
|
|
23
|
+
storageKey,
|
|
24
|
+
hasVoted,
|
|
25
|
+
fetchResultsImpl,
|
|
26
|
+
apiRoute,
|
|
27
|
+
onSuccess,
|
|
28
|
+
...formProps
|
|
29
|
+
}: PollProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { Poll, PollProps };
|
|
32
|
+
//# sourceMappingURL=Poll.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Form } from "./Form.js";
|
|
3
|
+
import { FormResults } from "./FormResults.js";
|
|
4
|
+
import { fetchFormResults } from "./fetchResults.js";
|
|
5
|
+
import { useCallback, useEffect, useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/react/Poll.tsx
|
|
8
|
+
const readVoted = (key) => {
|
|
9
|
+
try {
|
|
10
|
+
return window.localStorage.getItem(key) != null;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const writeVoted = (key) => {
|
|
16
|
+
try {
|
|
17
|
+
window.localStorage.setItem(key, "1");
|
|
18
|
+
} catch {}
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A poll: renders `<Form>` until the visitor votes, then fetches the aggregate results and shows
|
|
22
|
+
* `<FormResults>`. A per-browser localStorage flag (`storageKey`) skips straight to results on revisit. The
|
|
23
|
+
* guard is UX, not integrity (bypassable): server-enforced one-per-identity dedup composes via `req.user`
|
|
24
|
+
* (authed forms) or a `notAlreadySubmitted` rule, with cookie/IP identity deferred to the spam phase.
|
|
25
|
+
*/
|
|
26
|
+
const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchFormResults, apiRoute, onSuccess, ...formProps }) => {
|
|
27
|
+
const key = storageKey ?? `fb-poll-${formProps.form.id}`;
|
|
28
|
+
const [voted, setVoted] = useState(false);
|
|
29
|
+
const [results, setResults] = useState(null);
|
|
30
|
+
const loadResults = useCallback(async () => {
|
|
31
|
+
const result = await fetchResultsImpl({
|
|
32
|
+
formId: formProps.form.id,
|
|
33
|
+
field: resultsField,
|
|
34
|
+
apiRoute
|
|
35
|
+
});
|
|
36
|
+
setResults(result.ok ? result.results : []);
|
|
37
|
+
}, [
|
|
38
|
+
fetchResultsImpl,
|
|
39
|
+
formProps.form.id,
|
|
40
|
+
resultsField,
|
|
41
|
+
apiRoute
|
|
42
|
+
]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (hasVoted ?? readVoted(key)) {
|
|
45
|
+
setVoted(true);
|
|
46
|
+
loadResults();
|
|
47
|
+
}
|
|
48
|
+
}, [
|
|
49
|
+
hasVoted,
|
|
50
|
+
key,
|
|
51
|
+
loadResults
|
|
52
|
+
]);
|
|
53
|
+
const handleSuccess = useCallback((submissionId) => {
|
|
54
|
+
writeVoted(key);
|
|
55
|
+
setVoted(true);
|
|
56
|
+
loadResults();
|
|
57
|
+
onSuccess?.(submissionId);
|
|
58
|
+
}, [
|
|
59
|
+
key,
|
|
60
|
+
loadResults,
|
|
61
|
+
onSuccess
|
|
62
|
+
]);
|
|
63
|
+
if (voted) return results ? /* @__PURE__ */ jsx(FormResults, {
|
|
64
|
+
results,
|
|
65
|
+
t: formProps.t,
|
|
66
|
+
locale: formProps.locale
|
|
67
|
+
}) : null;
|
|
68
|
+
return /* @__PURE__ */ jsx(Form, {
|
|
69
|
+
...formProps,
|
|
70
|
+
apiRoute,
|
|
71
|
+
onSuccess: handleSuccess
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { Poll };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=Poll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { Form, type FormProps } from './Form'\nimport { FormResults } from './FormResults'\nimport { type FetchResultsResult, fetchFormResults } from './fetchResults'\n\nexport type PollProps = FormProps & {\n\t/** The choice field whose results are shown after voting (should match the form's public `resultsField`). */\n\tresultsField: string\n\t/** localStorage key for the per-browser voted guard. Default `fb-poll-{form.id}`. */\n\tstorageKey?: string\n\t/** Force the voted state (controlled). Omit to use the localStorage guard. */\n\thasVoted?: boolean\n\t/**\n\t * Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module\n\t * scope, `useCallback`, or `useMemo`): an inline function re-runs the load effect and double-fetches.\n\t */\n\tfetchResultsImpl?: typeof fetchFormResults\n}\n\nconst readVoted = (key: string): boolean => {\n\ttry {\n\t\treturn window.localStorage.getItem(key) != null\n\t} catch {\n\t\treturn false\n\t}\n}\n\nconst writeVoted = (key: string): void => {\n\ttry {\n\t\twindow.localStorage.setItem(key, '1')\n\t} catch {\n\t\t// Private mode / storage disabled: the guard is best-effort UX, never integrity.\n\t}\n}\n\n/**\n * A poll: renders `<Form>` until the visitor votes, then fetches the aggregate results and shows\n * `<FormResults>`. A per-browser localStorage flag (`storageKey`) skips straight to results on revisit. The\n * guard is UX, not integrity (bypassable): server-enforced one-per-identity dedup composes via `req.user`\n * (authed forms) or a `notAlreadySubmitted` rule, with cookie/IP identity deferred to the spam phase.\n */\nexport const Poll = ({\n\tresultsField,\n\tstorageKey,\n\thasVoted,\n\tfetchResultsImpl = fetchFormResults,\n\tapiRoute,\n\tonSuccess,\n\t...formProps\n}: PollProps) => {\n\tconst key = storageKey ?? `fb-poll-${formProps.form.id}`\n\tconst [voted, setVoted] = useState(false)\n\tconst [results, setResults] = useState<FieldAggregation[] | null>(null)\n\n\tconst loadResults = useCallback(async () => {\n\t\tconst result: FetchResultsResult = await fetchResultsImpl({\n\t\t\tformId: formProps.form.id,\n\t\t\tfield: resultsField,\n\t\t\tapiRoute,\n\t\t})\n\t\tsetResults(result.ok ? result.results : [])\n\t}, [fetchResultsImpl, formProps.form.id, resultsField, apiRoute])\n\n\tuseEffect(() => {\n\t\tconst already = hasVoted ?? readVoted(key)\n\t\tif (already) {\n\t\t\tsetVoted(true)\n\t\t\tvoid loadResults()\n\t\t}\n\t}, [hasVoted, key, loadResults])\n\n\tconst handleSuccess = useCallback(\n\t\t(submissionId?: string) => {\n\t\t\twriteVoted(key)\n\t\t\tsetVoted(true)\n\t\t\tvoid loadResults()\n\t\t\tonSuccess?.(submissionId)\n\t\t},\n\t\t[key, loadResults, onSuccess]\n\t)\n\n\tif (voted) {\n\t\treturn results ? (\n\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t) : null\n\t}\n\n\treturn <Form {...formProps} apiRoute={apiRoute} onSuccess={handleSuccess} />\n}\n"],"mappings":";;;;;;;AAsBA,MAAM,aAAa,QAAyB;CAC3C,IAAI;EACH,OAAO,OAAO,aAAa,QAAQ,GAAG,KAAK;CAC5C,QAAQ;EACP,OAAO;CACR;AACD;AAEA,MAAM,cAAc,QAAsB;CACzC,IAAI;EACH,OAAO,aAAa,QAAQ,KAAK,GAAG;CACrC,QAAQ,CAER;AACD;;;;;;;AAQA,MAAa,QAAQ,EACpB,cACA,YACA,UACA,mBAAmB,kBACnB,UACA,WACA,GAAG,gBACa;CAChB,MAAM,MAAM,cAAc,WAAW,UAAU,KAAK;CACpD,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,SAAS,cAAc,SAAoC,IAAI;CAEtE,MAAM,cAAc,YAAY,YAAY;EAC3C,MAAM,SAA6B,MAAM,iBAAiB;GACzD,QAAQ,UAAU,KAAK;GACvB,OAAO;GACP;EACD,CAAC;EACD,WAAW,OAAO,KAAK,OAAO,UAAU,CAAC,CAAC;CAC3C,GAAG;EAAC;EAAkB,UAAU,KAAK;EAAI;EAAc;CAAQ,CAAC;CAEhE,gBAAgB;EAEf,IADgB,YAAY,UAAU,GAAG,GAC5B;GACZ,SAAS,IAAI;GACb,YAAiB;EAClB;CACD,GAAG;EAAC;EAAU;EAAK;CAAW,CAAC;CAE/B,MAAM,gBAAgB,aACpB,iBAA0B;EAC1B,WAAW,GAAG;EACd,SAAS,IAAI;EACb,YAAiB;EACjB,YAAY,YAAY;CACzB,GACA;EAAC;EAAK;EAAa;CAAS,CAC7B;CAEA,IAAI,OACH,OAAO,UACN,oBAAC,aAAD;EAAsB;EAAS,GAAG,UAAU;EAAG,QAAQ,UAAU;CAAS,CAAA,IACvE;CAGL,OAAO,oBAAC,MAAD;EAAM,GAAI;EAAqB;EAAU,WAAW;CAAgB,CAAA;AAC5E"}
|
package/dist/react/cn.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cn.js","names":[],"sources":["../../src/react/cn.ts"],"sourcesContent":["'use client'\n\n/** Joins class names, filtering out falsy values. */\nexport const cn = (...classes: (string | undefined | null | false)[]): string =>\n\tclasses.filter(Boolean).join(' ')\n"],"mappings":";;;AAGA,MAAa,MAAM,GAAG,YACrB,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FormFieldInstance } from "../submissions/types.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/contract.d.ts
|
|
5
|
+
/** A localized translator the renderer uses for built-in copy. `(key) => string`; unknown keys return the key. */
|
|
6
|
+
type RendererTranslate = (key: string) => string;
|
|
7
|
+
/**
|
|
8
|
+
* The props every field renderer receives. The renderer turns one field instance plus its current value
|
|
9
|
+
* into an accessible control. Supplied by `<Form>` or directly in tests. Generic over the
|
|
10
|
+
* value type so custom renderers can narrow it.
|
|
11
|
+
*/
|
|
12
|
+
type FieldRendererProps<TValue = unknown> = {
|
|
13
|
+
/** The stored field instance (name, label, blockType, required, placeholder, description, options, ...). */field: FormFieldInstance; /** The control's stable id, for label/aria wiring. */
|
|
14
|
+
id: string; /** The field machine name (form value key). */
|
|
15
|
+
name: string;
|
|
16
|
+
value: TValue | undefined;
|
|
17
|
+
onChange: (value: TValue) => void;
|
|
18
|
+
onBlur: () => void; /** Blocking error messages for this field (already filtered to error severity). */
|
|
19
|
+
errors: string[]; /** Advisory messages (warning severity). */
|
|
20
|
+
warnings?: string[];
|
|
21
|
+
required: boolean;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
locale: string;
|
|
24
|
+
t: RendererTranslate;
|
|
25
|
+
};
|
|
26
|
+
/** A field renderer: maps `FieldRendererProps` to React output. */
|
|
27
|
+
type FieldRenderer<TValue = unknown> = (props: FieldRendererProps<TValue>) => ReactNode;
|
|
28
|
+
/** Identity helper that pins a renderer to the `FieldRenderer` type so authors get prop inference. */
|
|
29
|
+
declare const defineFieldRenderer: <TValue = unknown>(renderer: FieldRenderer<TValue>) => FieldRenderer<TValue>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { FieldRenderer, FieldRendererProps, RendererTranslate, defineFieldRenderer };
|
|
32
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/react/contract.ts
|
|
2
|
+
/** Identity helper that pins a renderer to the `FieldRenderer` type so authors get prop inference. */
|
|
3
|
+
const defineFieldRenderer = (renderer) => renderer;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { defineFieldRenderer };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","names":[],"sources":["../../src/react/contract.ts"],"sourcesContent":["import type { ReactNode } from 'react'\nimport type { FormFieldInstance } from '../submissions/types'\n\n/** A localized translator the renderer uses for built-in copy. `(key) => string`; unknown keys return the key. */\nexport type RendererTranslate = (key: string) => string\n\n/**\n * The props every field renderer receives. The renderer turns one field instance plus its current value\n * into an accessible control. Supplied by `<Form>` or directly in tests. Generic over the\n * value type so custom renderers can narrow it.\n */\nexport type FieldRendererProps<TValue = unknown> = {\n\t/** The stored field instance (name, label, blockType, required, placeholder, description, options, ...). */\n\tfield: FormFieldInstance\n\t/** The control's stable id, for label/aria wiring. */\n\tid: string\n\t/** The field machine name (form value key). */\n\tname: string\n\tvalue: TValue | undefined\n\tonChange: (value: TValue) => void\n\tonBlur: () => void\n\t/** Blocking error messages for this field (already filtered to error severity). */\n\terrors: string[]\n\t/** Advisory messages (warning severity). */\n\twarnings?: string[]\n\trequired: boolean\n\tdisabled?: boolean\n\tlocale: string\n\tt: RendererTranslate\n}\n\n/** A field renderer: maps `FieldRendererProps` to React output. */\nexport type FieldRenderer<TValue = unknown> = (props: FieldRendererProps<TValue>) => ReactNode\n\n/** Identity helper that pins a renderer to the `FieldRenderer` type so authors get prop inference. */\nexport const defineFieldRenderer = <TValue = unknown>(\n\trenderer: FieldRenderer<TValue>\n): FieldRenderer<TValue> => renderer\n"],"mappings":";;AAmCA,MAAa,uBACZ,aAC2B"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/react/events.ts
|
|
3
|
+
/** Emit a lifecycle event through the sink, stamping `formId` + an ISO `at`. */
|
|
4
|
+
const emitFormEvent = (sink, formId, event) => {
|
|
5
|
+
sink.emit({
|
|
6
|
+
...event,
|
|
7
|
+
formId,
|
|
8
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { emitFormEvent };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../src/react/events.ts"],"sourcesContent":["'use client'\n\nimport type { FormEvent, FormEventSink } from '../events/types'\n\n/** Distribute `Omit` across the event union so each variant keeps its discriminated extras. */\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never\n\n/** Emit a lifecycle event through the sink, stamping `formId` + an ISO `at`. */\nexport const emitFormEvent = (\n\tsink: FormEventSink,\n\tformId: string,\n\tevent: DistributiveOmit<FormEvent, 'formId' | 'at'>\n): void => {\n\tvoid sink.emit({ ...event, formId, at: new Date().toISOString() } as FormEvent)\n}\n"],"mappings":";;;AAQA,MAAa,iBACZ,MACA,QACA,UACU;CACV,KAAU,KAAK;EAAE,GAAG;EAAO;EAAQ,qBAAI,IAAI,KAAK,GAAE,YAAY;CAAE,CAAc;AAC/E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FieldAggregation } from "../aggregation/types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/react/fetchResults.d.ts
|
|
4
|
+
type FetchResultsInput = {
|
|
5
|
+
formId: number | string; /** Restrict to one field. Omit to get every public/enumerable field. */
|
|
6
|
+
field?: string; /** Payload API route prefix; defaults to `/api`. */
|
|
7
|
+
apiRoute?: string; /** Injectable for testing; defaults to global `fetch`. */
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
};
|
|
10
|
+
type FetchResultsResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
results: FieldAggregation[];
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
message?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Fetch aggregate poll/survey results from the form-builder results endpoint
|
|
19
|
+
* (`GET {apiRoute}/forms/:id/results`). Returns the server-resolved aggregations; the endpoint gates public
|
|
20
|
+
* access by the form's `showResults` opt-in. Pure: inject `fetchImpl` in tests.
|
|
21
|
+
*/
|
|
22
|
+
declare const fetchFormResults: (input: FetchResultsInput) => Promise<FetchResultsResult>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { FetchResultsInput, FetchResultsResult, fetchFormResults };
|
|
25
|
+
//# sourceMappingURL=fetchResults.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/react/fetchResults.ts
|
|
2
|
+
/**
|
|
3
|
+
* Fetch aggregate poll/survey results from the form-builder results endpoint
|
|
4
|
+
* (`GET {apiRoute}/forms/:id/results`). Returns the server-resolved aggregations; the endpoint gates public
|
|
5
|
+
* access by the form's `showResults` opt-in. Pure: inject `fetchImpl` in tests.
|
|
6
|
+
*/
|
|
7
|
+
const fetchFormResults = async (input) => {
|
|
8
|
+
const { formId, field, apiRoute = "/api", fetchImpl = fetch } = input;
|
|
9
|
+
const query = field ? `?field=${encodeURIComponent(field)}` : "";
|
|
10
|
+
let response;
|
|
11
|
+
try {
|
|
12
|
+
response = await fetchImpl(`${apiRoute}/forms/${formId}/results${query}`, { method: "GET" });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
message: error instanceof Error ? error.message : "Network error"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (!response.ok) return {
|
|
20
|
+
ok: false,
|
|
21
|
+
message: `Request failed (${response.status})`
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
results: (await response.json().catch(() => ({}))).results ?? []
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { fetchFormResults };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=fetchResults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetchResults.js","names":[],"sources":["../../src/react/fetchResults.ts"],"sourcesContent":["import type { FieldAggregation } from '../aggregation/types'\n\nexport type FetchResultsInput = {\n\tformId: number | string\n\t/** Restrict to one field. Omit to get every public/enumerable field. */\n\tfield?: string\n\t/** Payload API route prefix; defaults to `/api`. */\n\tapiRoute?: string\n\t/** Injectable for testing; defaults to global `fetch`. */\n\tfetchImpl?: typeof fetch\n}\n\nexport type FetchResultsResult =\n\t| { ok: true; results: FieldAggregation[] }\n\t| { ok: false; message?: string }\n\n/**\n * Fetch aggregate poll/survey results from the form-builder results endpoint\n * (`GET {apiRoute}/forms/:id/results`). Returns the server-resolved aggregations; the endpoint gates public\n * access by the form's `showResults` opt-in. Pure: inject `fetchImpl` in tests.\n */\nexport const fetchFormResults = async (input: FetchResultsInput): Promise<FetchResultsResult> => {\n\tconst { formId, field, apiRoute = '/api', fetchImpl = fetch } = input\n\tconst query = field ? `?field=${encodeURIComponent(field)}` : ''\n\tlet response: Response\n\ttry {\n\t\tresponse = await fetchImpl(`${apiRoute}/forms/${formId}/results${query}`, { method: 'GET' })\n\t} catch (error) {\n\t\treturn { ok: false, message: error instanceof Error ? error.message : 'Network error' }\n\t}\n\tif (!response.ok) {\n\t\treturn { ok: false, message: `Request failed (${response.status})` }\n\t}\n\tconst body = (await response.json().catch(() => ({}))) as { results?: FieldAggregation[] }\n\treturn { ok: true, results: body.results ?? [] }\n}\n"],"mappings":";;;;;;AAqBA,MAAa,mBAAmB,OAAO,UAA0D;CAChG,MAAM,EAAE,QAAQ,OAAO,WAAW,QAAQ,YAAY,UAAU;CAChE,MAAM,QAAQ,QAAQ,UAAU,mBAAmB,KAAK,MAAM;CAC9D,IAAI;CACJ,IAAI;EACH,WAAW,MAAM,UAAU,GAAG,SAAS,SAAS,OAAO,UAAU,SAAS,EAAE,QAAQ,MAAM,CAAC;CAC5F,SAAS,OAAO;EACf,OAAO;GAAE,IAAI;GAAO,SAAS,iBAAiB,QAAQ,MAAM,UAAU;EAAgB;CACvF;CACA,IAAI,CAAC,SAAS,IACb,OAAO;EAAE,IAAI;EAAO,SAAS,mBAAmB,SAAS,OAAO;CAAG;CAGpE,OAAO;EAAE,IAAI;EAAM,UAAS,MADR,SAAS,KAAK,EAAE,aAAa,CAAC,EAAE,GACnB,WAAW,CAAC;CAAE;AAChD"}
|