@10x-media/form-builder 0.1.0-beta.2 → 0.1.0-beta.4

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 (272) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +4 -4
  3. package/dist/actions/body/converters.d.ts +20 -0
  4. package/dist/actions/body/converters.js +57 -0
  5. package/dist/actions/body/converters.js.map +1 -0
  6. package/dist/actions/body/escapeHtml.d.ts +6 -0
  7. package/dist/actions/body/escapeHtml.js +14 -0
  8. package/dist/actions/body/escapeHtml.js.map +1 -0
  9. package/dist/actions/body/serializeBody.d.ts +50 -0
  10. package/dist/actions/body/serializeBody.js +78 -0
  11. package/dist/actions/body/serializeBody.js.map +1 -0
  12. package/dist/actions/body/serializeSlate.js +31 -0
  13. package/dist/actions/body/serializeSlate.js.map +1 -0
  14. package/dist/actions/body/wildcards.d.ts +10 -0
  15. package/dist/actions/body/wildcards.js +33 -0
  16. package/dist/actions/body/wildcards.js.map +1 -0
  17. package/dist/actions/builtin/confirmation.js +40 -15
  18. package/dist/actions/builtin/confirmation.js.map +1 -1
  19. package/dist/actions/builtin/emailTeam.js +19 -12
  20. package/dist/actions/builtin/emailTeam.js.map +1 -1
  21. package/dist/actions/builtin/index.d.ts +13 -1
  22. package/dist/actions/builtin/index.js +18 -7
  23. package/dist/actions/builtin/index.js.map +1 -1
  24. package/dist/actions/builtin/signedWebhook.js +6 -2
  25. package/dist/actions/builtin/signedWebhook.js.map +1 -1
  26. package/dist/actions/defineAction.d.ts +2 -1
  27. package/dist/actions/defineAction.js.map +1 -1
  28. package/dist/actions/dispatch.js +2 -1
  29. package/dist/actions/dispatch.js.map +1 -1
  30. package/dist/actions/runActions.js +10 -1
  31. package/dist/actions/runActions.js.map +1 -1
  32. package/dist/actions/task.js +7 -5
  33. package/dist/actions/task.js.map +1 -1
  34. package/dist/aggregation/resolveResultsRequest.d.ts +22 -6
  35. package/dist/aggregation/resolveResultsRequest.js +18 -7
  36. package/dist/aggregation/resolveResultsRequest.js.map +1 -1
  37. package/dist/calc/normalizeCalc.js +5 -17
  38. package/dist/calc/normalizeCalc.js.map +1 -1
  39. package/dist/calc/types.d.ts +8 -2
  40. package/dist/calc/types.js +22 -0
  41. package/dist/calc/types.js.map +1 -0
  42. package/dist/client/ByteSizeField.d.ts +21 -0
  43. package/dist/client/ByteSizeField.js +56 -0
  44. package/dist/client/ByteSizeField.js.map +1 -0
  45. package/dist/client/ConditionBuilder.js +2 -1
  46. package/dist/client/ConditionBuilder.js.map +1 -1
  47. package/dist/client/FieldNameSelect.d.ts +20 -0
  48. package/dist/client/FieldNameSelect.js +58 -0
  49. package/dist/client/FieldNameSelect.js.map +1 -0
  50. package/dist/client/FlowBuilder.d.ts +0 -1
  51. package/dist/client/FlowBuilder.js +102 -59
  52. package/dist/client/FlowBuilder.js.map +1 -1
  53. package/dist/client/FormConditionField.js +1 -4
  54. package/dist/client/FormConditionField.js.map +1 -1
  55. package/dist/client/admin.css +234 -0
  56. package/dist/client/flowAuthoring.js +35 -0
  57. package/dist/client/flowAuthoring.js.map +1 -0
  58. package/dist/client/synthesizeClientField.js +6 -2
  59. package/dist/client/synthesizeClientField.js.map +1 -1
  60. package/dist/client/toStaticLabel.js +13 -0
  61. package/dist/client/toStaticLabel.js.map +1 -0
  62. package/dist/collections/formSubmissions.js +49 -9
  63. package/dist/collections/formSubmissions.js.map +1 -1
  64. package/dist/collections/forms.js +251 -88
  65. package/dist/collections/forms.js.map +1 -1
  66. package/dist/conditions/conditionType.js +9 -2
  67. package/dist/conditions/conditionType.js.map +1 -1
  68. package/dist/conditions/normalizeConditions.js +2 -1
  69. package/dist/conditions/normalizeConditions.js.map +1 -1
  70. package/dist/consent/builtin/index.d.ts +11 -1
  71. package/dist/consent/builtin/index.js +15 -5
  72. package/dist/consent/builtin/index.js.map +1 -1
  73. package/dist/consent/builtin/static.js +7 -3
  74. package/dist/consent/builtin/static.js.map +1 -1
  75. package/dist/exports/client.d.ts +3 -1
  76. package/dist/exports/client.js +3 -1
  77. package/dist/exports/react.d.ts +15 -6
  78. package/dist/exports/react.js +10 -2
  79. package/dist/exports/rsc.d.ts +6 -1
  80. package/dist/exports/rsc.js +6 -1
  81. package/dist/exports/types.d.ts +4 -4
  82. package/dist/fields/buildFieldBlocks.js +20 -15
  83. package/dist/fields/buildFieldBlocks.js.map +1 -1
  84. package/dist/fields/builtin/calculation.js +115 -2
  85. package/dist/fields/builtin/calculation.js.map +1 -1
  86. package/dist/fields/builtin/consent.js +8 -4
  87. package/dist/fields/builtin/consent.js.map +1 -1
  88. package/dist/fields/builtin/date.js +29 -0
  89. package/dist/fields/builtin/date.js.map +1 -0
  90. package/dist/fields/builtin/file.d.ts +15 -0
  91. package/dist/fields/builtin/file.js +87 -7
  92. package/dist/fields/builtin/file.js.map +1 -1
  93. package/dist/fields/builtin/index.d.ts +24 -0
  94. package/dist/fields/builtin/index.js +29 -8
  95. package/dist/fields/builtin/index.js.map +1 -1
  96. package/dist/fields/builtin/message.js +26 -0
  97. package/dist/fields/builtin/message.js.map +1 -0
  98. package/dist/fields/builtin/repeater.js +7 -3
  99. package/dist/fields/builtin/repeater.js.map +1 -1
  100. package/dist/fields/builtin/select.js +7 -3
  101. package/dist/fields/builtin/select.js.map +1 -1
  102. package/dist/fields/fieldNamesOfType.js +25 -0
  103. package/dist/fields/fieldNamesOfType.js.map +1 -0
  104. package/dist/fields/localizedIf.d.ts +13 -0
  105. package/dist/fields/localizedIf.js +12 -0
  106. package/dist/fields/localizedIf.js.map +1 -0
  107. package/dist/fields/sharedConfig.js +82 -57
  108. package/dist/fields/sharedConfig.js.map +1 -1
  109. package/dist/fields/types.d.ts +7 -2
  110. package/dist/flow/normalizeFlow.js +7 -1
  111. package/dist/flow/normalizeFlow.js.map +1 -1
  112. package/dist/flow/stepLabel.js +15 -0
  113. package/dist/flow/stepLabel.js.map +1 -0
  114. package/dist/form/pollState.d.ts +22 -0
  115. package/dist/form/pollState.js +17 -0
  116. package/dist/form/pollState.js.map +1 -0
  117. package/dist/form/toFormDocument.d.ts +60 -0
  118. package/dist/form/toFormDocument.js +39 -0
  119. package/dist/form/toFormDocument.js.map +1 -0
  120. package/dist/form/types.d.ts +48 -0
  121. package/dist/index.d.ts +81 -13
  122. package/dist/index.js +36 -15
  123. package/dist/index.js.map +1 -1
  124. package/dist/plugin/registerCollections.js +17 -37
  125. package/dist/plugin/registerCollections.js.map +1 -1
  126. package/dist/plugin/uploadsCollection.d.ts +11 -0
  127. package/dist/plugin/uploadsCollection.js +44 -0
  128. package/dist/plugin/uploadsCollection.js.map +1 -0
  129. package/dist/poll/applyPollOptions.js +17 -0
  130. package/dist/poll/applyPollOptions.js.map +1 -0
  131. package/dist/poll/buildPollOptionSourceFields.js +53 -0
  132. package/dist/poll/buildPollOptionSourceFields.js.map +1 -0
  133. package/dist/poll/definePollOptionSource.d.ts +35 -0
  134. package/dist/poll/definePollOptionSource.js +6 -0
  135. package/dist/poll/definePollOptionSource.js.map +1 -0
  136. package/dist/poll/registry.d.ts +16 -0
  137. package/dist/poll/registry.js +16 -0
  138. package/dist/poll/registry.js.map +1 -0
  139. package/dist/poll/resolvePollOptions.d.ts +28 -0
  140. package/dist/poll/resolvePollOptions.js +64 -0
  141. package/dist/poll/resolvePollOptions.js.map +1 -0
  142. package/dist/poll/resolvePollOutcome.d.ts +21 -0
  143. package/dist/poll/resolvePollOutcome.js +36 -0
  144. package/dist/poll/resolvePollOutcome.js.map +1 -0
  145. package/dist/prefill/valuesFromSearchParams.js +1 -1
  146. package/dist/prefill/valuesFromSearchParams.js.map +1 -1
  147. package/dist/presentations/types.d.ts +3 -2
  148. package/dist/react/Form.d.ts +13 -31
  149. package/dist/react/Form.js +98 -85
  150. package/dist/react/Form.js.map +1 -1
  151. package/dist/react/FormContext.d.ts +35 -1
  152. package/dist/react/FormContext.js.map +1 -1
  153. package/dist/react/FormControls.d.ts +55 -0
  154. package/dist/react/FormControls.js +56 -0
  155. package/dist/react/FormControls.js.map +1 -0
  156. package/dist/react/FormResults.d.ts +6 -3
  157. package/dist/react/FormResults.js +37 -29
  158. package/dist/react/FormResults.js.map +1 -1
  159. package/dist/react/FormSteps.d.ts +17 -0
  160. package/dist/react/FormSteps.js +35 -0
  161. package/dist/react/FormSteps.js.map +1 -0
  162. package/dist/react/Poll.d.ts +17 -6
  163. package/dist/react/Poll.js +66 -18
  164. package/dist/react/Poll.js.map +1 -1
  165. package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
  166. package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
  167. package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
  168. package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
  169. package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
  170. package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
  171. package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
  172. package/dist/react/captcha/TurnstileCaptcha.js +53 -0
  173. package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
  174. package/dist/react/captcha/types.d.ts +8 -0
  175. package/dist/react/captcha/useCaptchaScript.js +45 -0
  176. package/dist/react/captcha/useCaptchaScript.js.map +1 -0
  177. package/dist/react/captcha/vendors.js +8 -0
  178. package/dist/react/captcha/vendors.js.map +1 -0
  179. package/dist/react/fetchResults.d.ts +1 -1
  180. package/dist/react/fetchResults.js +1 -1
  181. package/dist/react/fetchResults.js.map +1 -1
  182. package/dist/react/primitives/Input.d.ts +1 -1
  183. package/dist/react/primitives/Input.js.map +1 -1
  184. package/dist/react/renderers/calculation.js +1 -1
  185. package/dist/react/renderers/checkbox.js +1 -1
  186. package/dist/react/renderers/consent.js +1 -1
  187. package/dist/react/renderers/date.js +37 -0
  188. package/dist/react/renderers/date.js.map +1 -0
  189. package/dist/react/renderers/email.js +1 -1
  190. package/dist/react/renderers/file.js +25 -8
  191. package/dist/react/renderers/file.js.map +1 -1
  192. package/dist/react/renderers/index.js +4 -0
  193. package/dist/react/renderers/index.js.map +1 -1
  194. package/dist/react/renderers/message.js +35 -0
  195. package/dist/react/renderers/message.js.map +1 -0
  196. package/dist/react/renderers/number.js +1 -1
  197. package/dist/react/renderers/repeater.js +1 -1
  198. package/dist/react/renderers/select.js +1 -1
  199. package/dist/react/renderers/text.js +1 -1
  200. package/dist/react/renderers/textarea.js +1 -1
  201. package/dist/react/state.d.ts +25 -1
  202. package/dist/react/state.js +14 -1
  203. package/dist/react/state.js.map +1 -1
  204. package/dist/react/uploadFile.d.ts +2 -2
  205. package/dist/react/uploadFile.js +1 -1
  206. package/dist/react/uploadFile.js.map +1 -1
  207. package/dist/spam/captcha.d.ts +3 -2
  208. package/dist/spam/captcha.js +3 -2
  209. package/dist/spam/captcha.js.map +1 -1
  210. package/dist/spam/clientIp.js +9 -0
  211. package/dist/spam/clientIp.js.map +1 -0
  212. package/dist/spam/identify.js +3 -6
  213. package/dist/spam/identify.js.map +1 -1
  214. package/dist/spam/providers/hcaptcha.d.ts +17 -0
  215. package/dist/spam/providers/hcaptcha.js +18 -0
  216. package/dist/spam/providers/hcaptcha.js.map +1 -0
  217. package/dist/spam/providers/recaptcha.d.ts +23 -0
  218. package/dist/spam/providers/recaptcha.js +23 -0
  219. package/dist/spam/providers/recaptcha.js.map +1 -0
  220. package/dist/spam/providers/siteverify.js +34 -0
  221. package/dist/spam/providers/siteverify.js.map +1 -0
  222. package/dist/spam/providers/turnstile.d.ts +17 -0
  223. package/dist/spam/providers/turnstile.js +18 -0
  224. package/dist/spam/providers/turnstile.js.map +1 -0
  225. package/dist/spam/spamGuard.js +2 -5
  226. package/dist/spam/spamGuard.js.map +1 -1
  227. package/dist/spam/types.d.ts +2 -2
  228. package/dist/submissions/runSubmission.js +70 -33
  229. package/dist/submissions/runSubmission.js.map +1 -1
  230. package/dist/submissions/validateSubmission.js +41 -7
  231. package/dist/submissions/validateSubmission.js.map +1 -1
  232. package/dist/submissions/votedCookie.d.ts +13 -0
  233. package/dist/submissions/votedCookie.js +27 -0
  234. package/dist/submissions/votedCookie.js.map +1 -0
  235. package/dist/translations/de.js +219 -0
  236. package/dist/translations/de.js.map +1 -0
  237. package/dist/translations/en.js +71 -10
  238. package/dist/translations/en.js.map +1 -1
  239. package/dist/translations/index.d.ts +2 -1
  240. package/dist/translations/index.js +6 -2
  241. package/dist/translations/index.js.map +1 -1
  242. package/dist/translations/keys.d.ts +69 -8
  243. package/dist/translations/keys.js +70 -9
  244. package/dist/translations/keys.js.map +1 -1
  245. package/dist/uploads/formatBytes.d.ts +9 -0
  246. package/dist/uploads/formatBytes.js +25 -0
  247. package/dist/uploads/formatBytes.js.map +1 -0
  248. package/dist/uploads/resolveFileRef.js +1 -1
  249. package/dist/uploads/resolveFileRef.js.map +1 -1
  250. package/dist/uploads/types.d.ts +1 -2
  251. package/dist/validation/builtin/index.d.ts +14 -0
  252. package/dist/validation/builtin/index.js +12 -1
  253. package/dist/validation/builtin/index.js.map +1 -1
  254. package/dist/validation/builtin/maxDate.js +23 -0
  255. package/dist/validation/builtin/maxDate.js.map +1 -0
  256. package/dist/validation/builtin/minDate.js +23 -0
  257. package/dist/validation/builtin/minDate.js.map +1 -0
  258. package/dist/validation/builtin/validateDateBound.js +18 -0
  259. package/dist/validation/builtin/validateDateBound.js.map +1 -0
  260. package/dist/validation/validateUrl.js +22 -0
  261. package/dist/validation/validateUrl.js.map +1 -0
  262. package/package.json +4 -2
  263. package/styles/form-builder.css +10 -190
  264. package/dist/collections/uploads.d.ts +0 -30
  265. package/dist/collections/uploads.js +0 -55
  266. package/dist/collections/uploads.js.map +0 -1
  267. package/dist/presentations/registry.d.ts +0 -15
  268. package/dist/presentations/registry.js +0 -15
  269. package/dist/presentations/registry.js.map +0 -1
  270. package/dist/react/toFormDocument.d.ts +0 -24
  271. package/dist/react/toFormDocument.js +0 -22
  272. package/dist/react/toFormDocument.js.map +0 -1
@@ -1 +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 { RendererRegistry } from './registry'\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\t/** The active renderer registry, exposed so nested renderers (e.g. repeater) can look up sub-renderers. */\n\trendererRegistry: RendererRegistry\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":";;;AA+BA,MAAa,cAAc,cAAuC,IAAI;;AAGtE,MAAa,uBAAyC;CACrD,MAAM,UAAU,WAAW,WAAW;CACtC,IAAI,CAAC,SACJ,MAAM,IAAI,MAAM,6CAA6C;CAE9D,OAAO;AACR"}
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 { RendererTranslate } from './contract'\nimport type { RendererRegistry } from './registry'\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\n/** Resolved chrome button labels. Precedence per label: the `<Form>` prop, then (submit only) the form's `response.submitLabel`, then the translated default. */\nexport type FormControlLabels = {\n\tback: string\n\tnext: string\n\tsubmit: string\n}\n\n/**\n * The form controller's context, read via `useFormContext` (or the focused `useField`,\n * `useFormState`, and `useFormStep` hooks). Available anywhere under `<Form>`, including\n * custom `children` layouts and custom field renderers.\n */\nexport type FormContextValue = {\n\t/** Current form state: values, errors, warnings, touched, submitting, submitted, submitError. */\n\tstate: FormState\n\t/**\n\t * Dispatch a `FormAction` (see `./state` for the action union). Custom field layouts\n\t * typically dispatch `TOUCH` or `SET_FIELD_ISSUES`; prefer `useField` for value binding,\n\t * which wires `SET_VALUE` and validation for you.\n\t */\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\t/** The locale passed to `<Form>` (defaults to `'en'`). */\n\tlocale: string\n\t/** Multi-step navigation state and the `goNext`/`goBack` handlers. */\n\tstep: FormStepInfo\n\t/** The active renderer registry, exposed so nested renderers (e.g. repeater) can look up sub-renderers. */\n\trendererRegistry: RendererRegistry\n\t/** Resolved back/next/submit labels used by `<FormControls>` and available to custom chrome. */\n\tlabels: FormControlLabels\n\t/** The active translator: the `<Form>` `t` prop, else the bundled English fallback. */\n\tt: RendererTranslate\n\t/** Calc-authoritative answers: `state.values` overlaid with derived calc values. Consumers fall back to `state.values` when absent. */\n\teffectiveValues?: Record<string, unknown>\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":";;;AA0DA,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,55 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/react/FormControls.d.ts
4
+ /** Props passed to `renderSubmit`. */
5
+ type SubmitButtonRenderProps = {
6
+ label: string;
7
+ submitting: boolean;
8
+ };
9
+ /** Props passed to `renderNext`. */
10
+ type NextButtonRenderProps = {
11
+ label: string;
12
+ submitting: boolean;
13
+ onClick: () => void;
14
+ };
15
+ /** Props passed to `renderBack`. */
16
+ type BackButtonRenderProps = {
17
+ label: string;
18
+ submitting: boolean;
19
+ onClick: () => void;
20
+ };
21
+ type FormControlsProps = {
22
+ /** Override the context-resolved back label for this instance. */backLabel?: string; /** Override the context-resolved next label for this instance. */
23
+ nextLabel?: string; /** Override the context-resolved submit label for this instance. */
24
+ submitLabel?: string; /** Additional CSS class on the wrapping `.fb-form__controls` element. */
25
+ className?: string; /** CSS class forwarded to the default "Back" `<button>`. Ignored when `renderBack` is provided. */
26
+ backButtonClassName?: string; /** CSS class forwarded to the default "Next" `<button>`. Ignored when `renderNext` is provided. */
27
+ nextButtonClassName?: string; /** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */
28
+ submitButtonClassName?: string; /** Replace the default "Back" button entirely. */
29
+ renderBack?: (props: BackButtonRenderProps) => ReactNode; /** Replace the default "Next" button entirely. */
30
+ renderNext?: (props: NextButtonRenderProps) => ReactNode; /** Replace the default submit button entirely. Receives the resolved label and submitting state. */
31
+ renderSubmit?: (props: SubmitButtonRenderProps) => ReactNode;
32
+ };
33
+ /**
34
+ * The form's navigation chrome: back on non-first steps, next on non-terminal steps, submit on
35
+ * the terminal step (a single-step form is first and terminal, so it gets only submit). Driven
36
+ * entirely by the form context, so it composes into custom `children` layouts exactly like the
37
+ * default field loop's chrome. Labels resolve from context (`<Form>` props, the form's
38
+ * `response.submitLabel`, translated defaults) unless overridden per button here. Must render
39
+ * inside `<Form>`, within its `<form>` element so the submit button submits.
40
+ */
41
+ declare const FormControls: ({
42
+ backLabel,
43
+ nextLabel,
44
+ submitLabel,
45
+ className,
46
+ backButtonClassName,
47
+ nextButtonClassName,
48
+ submitButtonClassName,
49
+ renderBack,
50
+ renderNext,
51
+ renderSubmit
52
+ }: FormControlsProps) => import("react/jsx-runtime").JSX.Element;
53
+ //#endregion
54
+ export { BackButtonRenderProps, FormControls, FormControlsProps, NextButtonRenderProps, SubmitButtonRenderProps };
55
+ //# sourceMappingURL=FormControls.d.ts.map
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { cn } from "./cn.js";
3
+ import { useFormContext } from "./FormContext.js";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/react/FormControls.tsx
6
+ /**
7
+ * The form's navigation chrome: back on non-first steps, next on non-terminal steps, submit on
8
+ * the terminal step (a single-step form is first and terminal, so it gets only submit). Driven
9
+ * entirely by the form context, so it composes into custom `children` layouts exactly like the
10
+ * default field loop's chrome. Labels resolve from context (`<Form>` props, the form's
11
+ * `response.submitLabel`, translated defaults) unless overridden per button here. Must render
12
+ * inside `<Form>`, within its `<form>` element so the submit button submits.
13
+ */
14
+ const FormControls = ({ backLabel, nextLabel, submitLabel, className, backButtonClassName, nextButtonClassName, submitButtonClassName, renderBack, renderNext, renderSubmit }) => {
15
+ const { state, step, labels } = useFormContext();
16
+ const submitting = state.submitting;
17
+ const back = backLabel ?? labels.back;
18
+ const next = nextLabel ?? labels.next;
19
+ const submit = submitLabel ?? labels.submit;
20
+ return /* @__PURE__ */ jsxs("div", {
21
+ className: cn("fb-form__controls", className),
22
+ children: [!step.isFirst ? renderBack ? renderBack({
23
+ label: back,
24
+ submitting,
25
+ onClick: step.goBack
26
+ }) : /* @__PURE__ */ jsx("button", {
27
+ type: "button",
28
+ className: cn("fb-form__back", backButtonClassName),
29
+ onClick: step.goBack,
30
+ disabled: submitting,
31
+ children: back
32
+ }) : null, step.isTerminal ? renderSubmit ? renderSubmit({
33
+ label: submit,
34
+ submitting
35
+ }) : /* @__PURE__ */ jsx("button", {
36
+ type: "submit",
37
+ className: cn("fb-form__submit", submitButtonClassName),
38
+ disabled: submitting,
39
+ children: submit
40
+ }) : renderNext ? renderNext({
41
+ label: next,
42
+ submitting,
43
+ onClick: step.goNext
44
+ }) : /* @__PURE__ */ jsx("button", {
45
+ type: "button",
46
+ className: cn("fb-form__next", nextButtonClassName),
47
+ onClick: step.goNext,
48
+ disabled: submitting,
49
+ children: next
50
+ })]
51
+ });
52
+ };
53
+ //#endregion
54
+ export { FormControls };
55
+
56
+ //# sourceMappingURL=FormControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormControls.js","names":[],"sources":["../../src/react/FormControls.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\nimport { cn } from './cn'\nimport { useFormContext } from './FormContext'\n\n/** Props passed to `renderSubmit`. */\nexport type SubmitButtonRenderProps = {\n\tlabel: string\n\tsubmitting: boolean\n}\n\n/** Props passed to `renderNext`. */\nexport type NextButtonRenderProps = {\n\tlabel: string\n\tsubmitting: boolean\n\tonClick: () => void\n}\n\n/** Props passed to `renderBack`. */\nexport type BackButtonRenderProps = {\n\tlabel: string\n\tsubmitting: boolean\n\tonClick: () => void\n}\n\nexport type FormControlsProps = {\n\t/** Override the context-resolved back label for this instance. */\n\tbackLabel?: string\n\t/** Override the context-resolved next label for this instance. */\n\tnextLabel?: string\n\t/** Override the context-resolved submit label for this instance. */\n\tsubmitLabel?: string\n\t/** Additional CSS class on the wrapping `.fb-form__controls` element. */\n\tclassName?: string\n\t/** CSS class forwarded to the default \"Back\" `<button>`. Ignored when `renderBack` is provided. */\n\tbackButtonClassName?: string\n\t/** CSS class forwarded to the default \"Next\" `<button>`. Ignored when `renderNext` is provided. */\n\tnextButtonClassName?: string\n\t/** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */\n\tsubmitButtonClassName?: string\n\t/** Replace the default \"Back\" button entirely. */\n\trenderBack?: (props: BackButtonRenderProps) => ReactNode\n\t/** Replace the default \"Next\" button entirely. */\n\trenderNext?: (props: NextButtonRenderProps) => ReactNode\n\t/** Replace the default submit button entirely. Receives the resolved label and submitting state. */\n\trenderSubmit?: (props: SubmitButtonRenderProps) => ReactNode\n}\n\n/**\n * The form's navigation chrome: back on non-first steps, next on non-terminal steps, submit on\n * the terminal step (a single-step form is first and terminal, so it gets only submit). Driven\n * entirely by the form context, so it composes into custom `children` layouts exactly like the\n * default field loop's chrome. Labels resolve from context (`<Form>` props, the form's\n * `response.submitLabel`, translated defaults) unless overridden per button here. Must render\n * inside `<Form>`, within its `<form>` element so the submit button submits.\n */\nexport const FormControls = ({\n\tbackLabel,\n\tnextLabel,\n\tsubmitLabel,\n\tclassName,\n\tbackButtonClassName,\n\tnextButtonClassName,\n\tsubmitButtonClassName,\n\trenderBack,\n\trenderNext,\n\trenderSubmit,\n}: FormControlsProps) => {\n\tconst { state, step, labels } = useFormContext()\n\tconst submitting = state.submitting\n\tconst back = backLabel ?? labels.back\n\tconst next = nextLabel ?? labels.next\n\tconst submit = submitLabel ?? labels.submit\n\treturn (\n\t\t<div className={cn('fb-form__controls', className)}>\n\t\t\t{!step.isFirst ? (\n\t\t\t\trenderBack ? (\n\t\t\t\t\trenderBack({ label: back, submitting, onClick: step.goBack })\n\t\t\t\t) : (\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={cn('fb-form__back', backButtonClassName)}\n\t\t\t\t\t\tonClick={step.goBack}\n\t\t\t\t\t\tdisabled={submitting}\n\t\t\t\t\t>\n\t\t\t\t\t\t{back}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t) : null}\n\t\t\t{step.isTerminal ? (\n\t\t\t\trenderSubmit ? (\n\t\t\t\t\trenderSubmit({ label: submit, submitting })\n\t\t\t\t) : (\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\tclassName={cn('fb-form__submit', submitButtonClassName)}\n\t\t\t\t\t\tdisabled={submitting}\n\t\t\t\t\t>\n\t\t\t\t\t\t{submit}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t) : renderNext ? (\n\t\t\t\trenderNext({ label: next, submitting, onClick: step.goNext })\n\t\t\t) : (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName={cn('fb-form__next', nextButtonClassName)}\n\t\t\t\t\tonClick={step.goNext}\n\t\t\t\t\tdisabled={submitting}\n\t\t\t\t>\n\t\t\t\t\t{next}\n\t\t\t\t</button>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;AAyDA,MAAa,gBAAgB,EAC5B,WACA,WACA,aACA,WACA,qBACA,qBACA,uBACA,YACA,YACA,mBACwB;CACxB,MAAM,EAAE,OAAO,MAAM,WAAW,eAAe;CAC/C,MAAM,aAAa,MAAM;CACzB,MAAM,OAAO,aAAa,OAAO;CACjC,MAAM,OAAO,aAAa,OAAO;CACjC,MAAM,SAAS,eAAe,OAAO;CACrC,OACC,qBAAC,OAAD;EAAK,WAAW,GAAG,qBAAqB,SAAS;YAAjD,CACE,CAAC,KAAK,UACN,aACC,WAAW;GAAE,OAAO;GAAM;GAAY,SAAS,KAAK;EAAO,CAAC,IAE5D,oBAAC,UAAD;GACC,MAAK;GACL,WAAW,GAAG,iBAAiB,mBAAmB;GAClD,SAAS,KAAK;GACd,UAAU;aAET;EACM,CAAA,IAEN,MACH,KAAK,aACL,eACC,aAAa;GAAE,OAAO;GAAQ;EAAW,CAAC,IAE1C,oBAAC,UAAD;GACC,MAAK;GACL,WAAW,GAAG,mBAAmB,qBAAqB;GACtD,UAAU;aAET;EACM,CAAA,IAEN,aACH,WAAW;GAAE,OAAO;GAAM;GAAY,SAAS,KAAK;EAAO,CAAC,IAE5D,oBAAC,UAAD;GACC,MAAK;GACL,WAAW,GAAG,iBAAiB,mBAAmB;GAClD,SAAS,KAAK;GACd,UAAU;aAET;EACM,CAAA,CAEL;;AAEP"}
@@ -7,18 +7,21 @@ type FormResultsProps = {
7
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
8
  t?: RendererTranslate;
9
9
  locale?: string; /** Show the raw count beside each option. Default true. */
10
- showCounts?: boolean;
10
+ showCounts?: boolean; /** The recorded outcome's stable option value; the matching bucket is highlighted as the winner. */
11
+ winningValue?: string;
11
12
  };
12
13
  /**
13
14
  * Presentational results view for polls and survey summaries: per-option bars with percentages. Headless,
14
15
  * data resolved server-side via `aggregateFieldResponses`/`aggregateFormResponses` and passed in (it never
15
16
  * 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
+ * is `aria-hidden` visual sugar sized by inline width. With a `winningValue`, the matching bucket gets
18
+ * the winner class and a translated badge (real text, so the outcome is accessible).
17
19
  */
18
20
  declare const FormResults: ({
19
21
  results,
20
22
  t,
21
- showCounts
23
+ showCounts,
24
+ winningValue
22
25
  }: FormResultsProps) => ReactNode;
23
26
  //#endregion
24
27
  export { FormResults, FormResultsProps };
@@ -4,7 +4,7 @@ import { en } from "../translations/en.js";
4
4
  import { makeTranslate } from "../translations/makeTranslate.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/react/FormResults.tsx
7
- const OneResult = ({ result, t, showCounts }) => {
7
+ const OneResult = ({ result, t, showCounts, winningValue }) => {
8
8
  if (result.total === 0) return /* @__PURE__ */ jsxs("section", {
9
9
  className: "fb-results__field",
10
10
  children: [result.label ? /* @__PURE__ */ jsx("h3", {
@@ -24,31 +24,37 @@ const OneResult = ({ result, t, showCounts }) => {
24
24
  }) : null,
25
25
  /* @__PURE__ */ jsx("ul", {
26
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))
27
+ children: result.buckets.map((bucket) => {
28
+ const isWinner = winningValue != null && bucket.value === winningValue;
29
+ return /* @__PURE__ */ jsxs("li", {
30
+ className: isWinner ? "fb-results__row fb-results__bucket--winner" : "fb-results__row",
31
+ children: [
32
+ /* @__PURE__ */ jsxs("span", {
33
+ className: "fb-results__option",
34
+ children: [bucket.label, isWinner ? /* @__PURE__ */ jsx("span", {
35
+ className: "fb-results__winner-badge",
36
+ children: t(keys.resultsWinner)
37
+ }) : null]
38
+ }),
39
+ /* @__PURE__ */ jsx("span", {
40
+ className: "fb-results__bar",
41
+ children: /* @__PURE__ */ jsx("span", {
42
+ className: "fb-results__bar-fill",
43
+ style: { width: `${bucket.percentage}%` },
44
+ "aria-hidden": "true"
45
+ })
46
+ }),
47
+ /* @__PURE__ */ jsxs("span", {
48
+ className: "fb-results__pct",
49
+ children: [bucket.percentage, "%"]
50
+ }),
51
+ showCounts ? /* @__PURE__ */ jsx("span", {
52
+ className: "fb-results__count",
53
+ children: bucket.count
54
+ }) : null
55
+ ]
56
+ }, bucket.value);
57
+ })
52
58
  }),
53
59
  /* @__PURE__ */ jsxs("p", {
54
60
  className: "fb-results__total",
@@ -66,16 +72,18 @@ const OneResult = ({ result, t, showCounts }) => {
66
72
  * Presentational results view for polls and survey summaries: per-option bars with percentages. Headless,
67
73
  * data resolved server-side via `aggregateFieldResponses`/`aggregateFormResponses` and passed in (it never
68
74
  * 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.
75
+ * is `aria-hidden` visual sugar sized by inline width. With a `winningValue`, the matching bucket gets
76
+ * the winner class and a translated badge (real text, so the outcome is accessible).
70
77
  */
71
- const FormResults = ({ results, t, showCounts = true }) => {
78
+ const FormResults = ({ results, t, showCounts = true, winningValue }) => {
72
79
  const translate = t ?? makeTranslate(en);
73
80
  return /* @__PURE__ */ jsx("div", {
74
81
  className: "fb-results",
75
82
  children: (Array.isArray(results) ? results : [results]).map((result) => /* @__PURE__ */ jsx(OneResult, {
76
83
  result,
77
84
  t: translate,
78
- showCounts
85
+ showCounts,
86
+ winningValue
79
87
  }, result.field))
80
88
  });
81
89
  };
@@ -1 +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"}
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\t/** The recorded outcome's stable option value; the matching bucket is highlighted as the winner. */\n\twinningValue?: string\n}\n\nconst OneResult = ({\n\tresult,\n\tt,\n\tshowCounts,\n\twinningValue,\n}: {\n\tresult: FieldAggregation\n\tt: RendererTranslate\n\tshowCounts: boolean\n\twinningValue?: string\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\tconst isWinner = winningValue != null && bucket.value === winningValue\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<li\n\t\t\t\t\t\t\tkey={bucket.value}\n\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\tisWinner ? 'fb-results__row fb-results__bucket--winner' : 'fb-results__row'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"fb-results__option\">\n\t\t\t\t\t\t\t\t{bucket.label}\n\t\t\t\t\t\t\t\t{isWinner ? (\n\t\t\t\t\t\t\t\t\t<span className=\"fb-results__winner-badge\">{t(keys.resultsWinner)}</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span className=\"fb-results__bar\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclassName=\"fb-results__bar-fill\"\n\t\t\t\t\t\t\t\t\tstyle={{ width: `${bucket.percentage}%` }}\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span className=\"fb-results__pct\">{bucket.percentage}%</span>\n\t\t\t\t\t\t\t{showCounts ? <span className=\"fb-results__count\">{bucket.count}</span> : null}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t)\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. With a `winningValue`, the matching bucket gets\n * the winner class and a translated badge (real text, so the outcome is accessible).\n */\nexport const FormResults = ({\n\tresults,\n\tt,\n\tshowCounts = true,\n\twinningValue,\n}: 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\n\t\t\t\t\tkey={result.field}\n\t\t\t\t\tresult={result}\n\t\t\t\t\tt={translate}\n\t\t\t\t\tshowCounts={showCounts}\n\t\t\t\t\twinningValue={winningValue}\n\t\t\t\t/>\n\t\t\t))}\n\t\t</div>\n\t)\n}\n"],"mappings":";;;;;;AAqBA,MAAM,aAAa,EAClB,QACA,GACA,YACA,mBAMgB;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,WAAW;KAC/B,MAAM,WAAW,gBAAgB,QAAQ,OAAO,UAAU;KAC1D,OACC,qBAAC,MAAD;MAEC,WACC,WAAW,+CAA+C;gBAH5D;OAMC,qBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,OAAO,OACP,WACA,oBAAC,QAAD;SAAM,WAAU;mBAA4B,EAAE,KAAK,aAAa;QAAQ,CAAA,IACrE,IACC;;OACN,oBAAC,QAAD;QAAM,WAAU;kBACf,oBAAC,QAAD;SACC,WAAU;SACV,OAAO,EAAE,OAAO,GAAG,OAAO,WAAW,GAAG;SACxC,eAAY;QACZ,CAAA;OACI,CAAA;OACN,qBAAC,QAAD;QAAM,WAAU;kBAAhB,CAAmC,OAAO,YAAW,GAAO;;OAC3D,aAAa,oBAAC,QAAD;QAAM,WAAU;kBAAqB,OAAO;OAAY,CAAA,IAAI;MACvE;QApBE,OAAO,KAoBT;IAEN,CAAC;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;;;;;;;;AASA,MAAa,eAAe,EAC3B,SACA,GACA,aAAa,MACb,mBACkC;CAClC,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;GAES;GACR,GAAG;GACS;GACE;EACd,GALK,OAAO,KAKZ,CACD;CACG,CAAA;AAEP"}
@@ -0,0 +1,17 @@
1
+ //#region src/react/FormSteps.d.ts
2
+ type FormStepsProps = {
3
+ /** Additional CSS class on the wrapping `.fb-form-steps` list. */className?: string;
4
+ };
5
+ /**
6
+ * Step-progress indicator for multi-step forms: an ordered list of the flow's steps with the
7
+ * current one marked via `aria-current="step"` and a `--current` class. Step labels use the
8
+ * step's title, falling back to the translated "Step {n}" (same semantics as the admin flow
9
+ * builder). Carries no styling opinions beyond `fb-form-steps` classes. Renders nothing when
10
+ * the form has fewer than two steps. Must render inside `<Form>`.
11
+ */
12
+ declare const FormSteps: ({
13
+ className
14
+ }: FormStepsProps) => import("react/jsx-runtime").JSX.Element | null;
15
+ //#endregion
16
+ export { FormSteps, FormStepsProps };
17
+ //# sourceMappingURL=FormSteps.d.ts.map
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { keys } from "../translations/keys.js";
3
+ import { stepLabel } from "../flow/stepLabel.js";
4
+ import { cn } from "./cn.js";
5
+ import { useFormContext } from "./FormContext.js";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/react/FormSteps.tsx
8
+ /**
9
+ * Step-progress indicator for multi-step forms: an ordered list of the flow's steps with the
10
+ * current one marked via `aria-current="step"` and a `--current` class. Step labels use the
11
+ * step's title, falling back to the translated "Step {n}" (same semantics as the admin flow
12
+ * builder). Carries no styling opinions beyond `fb-form-steps` classes. Renders nothing when
13
+ * the form has fewer than two steps. Must render inside `<Form>`.
14
+ */
15
+ const FormSteps = ({ className }) => {
16
+ const { step, t } = useFormContext();
17
+ const steps = step.flow?.steps;
18
+ if (!steps || steps.length < 2) return null;
19
+ const fallbackTitle = t(keys.flowStepFallbackTitle);
20
+ return /* @__PURE__ */ jsx("ol", {
21
+ className: cn("fb-form-steps", className),
22
+ children: steps.map((flowStep, index) => {
23
+ const current = index === step.stepIndex;
24
+ return /* @__PURE__ */ jsx("li", {
25
+ className: cn("fb-form-steps__step", current && "fb-form-steps__step--current"),
26
+ "aria-current": current ? "step" : void 0,
27
+ children: stepLabel(flowStep, index, fallbackTitle)
28
+ }, flowStep.id);
29
+ })
30
+ });
31
+ };
32
+ //#endregion
33
+ export { FormSteps };
34
+
35
+ //# sourceMappingURL=FormSteps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormSteps.js","names":[],"sources":["../../src/react/FormSteps.tsx"],"sourcesContent":["'use client'\n\nimport { stepLabel } from '../flow/stepLabel'\nimport { keys } from '../translations/keys'\nimport { cn } from './cn'\nimport { useFormContext } from './FormContext'\n\nexport type FormStepsProps = {\n\t/** Additional CSS class on the wrapping `.fb-form-steps` list. */\n\tclassName?: string\n}\n\n/**\n * Step-progress indicator for multi-step forms: an ordered list of the flow's steps with the\n * current one marked via `aria-current=\"step\"` and a `--current` class. Step labels use the\n * step's title, falling back to the translated \"Step {n}\" (same semantics as the admin flow\n * builder). Carries no styling opinions beyond `fb-form-steps` classes. Renders nothing when\n * the form has fewer than two steps. Must render inside `<Form>`.\n */\nexport const FormSteps = ({ className }: FormStepsProps) => {\n\tconst { step, t } = useFormContext()\n\tconst steps = step.flow?.steps\n\tif (!steps || steps.length < 2) {\n\t\treturn null\n\t}\n\tconst fallbackTitle = t(keys.flowStepFallbackTitle)\n\treturn (\n\t\t<ol className={cn('fb-form-steps', className)}>\n\t\t\t{steps.map((flowStep, index) => {\n\t\t\t\tconst current = index === step.stepIndex\n\t\t\t\treturn (\n\t\t\t\t\t<li\n\t\t\t\t\t\tkey={flowStep.id}\n\t\t\t\t\t\tclassName={cn('fb-form-steps__step', current && 'fb-form-steps__step--current')}\n\t\t\t\t\t\taria-current={current ? 'step' : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t{stepLabel(flowStep, index, fallbackTitle)}\n\t\t\t\t\t</li>\n\t\t\t\t)\n\t\t\t})}\n\t\t</ol>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;AAmBA,MAAa,aAAa,EAAE,gBAAgC;CAC3D,MAAM,EAAE,MAAM,MAAM,eAAe;CACnC,MAAM,QAAQ,KAAK,MAAM;CACzB,IAAI,CAAC,SAAS,MAAM,SAAS,GAC5B,OAAO;CAER,MAAM,gBAAgB,EAAE,KAAK,qBAAqB;CAClD,OACC,oBAAC,MAAD;EAAI,WAAW,GAAG,iBAAiB,SAAS;YAC1C,MAAM,KAAK,UAAU,UAAU;GAC/B,MAAM,UAAU,UAAU,KAAK;GAC/B,OACC,oBAAC,MAAD;IAEC,WAAW,GAAG,uBAAuB,WAAW,8BAA8B;IAC9E,gBAAc,UAAU,SAAS,KAAA;cAEhC,UAAU,UAAU,OAAO,aAAa;GACtC,GALE,SAAS,EAKX;EAEN,CAAC;CACE,CAAA;AAEN"}
@@ -3,8 +3,13 @@ import { fetchFormResults } from "./fetchResults.js";
3
3
 
4
4
  //#region src/react/Poll.d.ts
5
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. */
6
+ /** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */resultsField: string; /** localStorage key for the per-browser voted guard. Default `fb-poll-{form.id}`. */
7
+ storageKey?: string;
8
+ /**
9
+ * Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;
10
+ * `false`/omitted falls back to localStorage. SSR hosts using the plugin's `poll.votedCookie`
11
+ * option pass `hasVotedCookie(cookieHeader, form.id)` here.
12
+ */
8
13
  hasVoted?: boolean;
9
14
  /**
10
15
  * Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module
@@ -13,10 +18,16 @@ type PollProps = FormProps & {
13
18
  fetchResultsImpl?: typeof fetchFormResults;
14
19
  };
15
20
  /**
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.
21
+ * A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows
22
+ * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated
23
+ * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open
24
+ * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A
25
+ * recorded `outcome.winningValue` (via `resolvePollOutcome`) supersedes everything: a translated final
26
+ * notice plus results with the winning bucket highlighted. A
27
+ * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`
28
+ * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not
29
+ * integrity (bypassable): server-enforced
30
+ * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.
20
31
  */
21
32
  declare const Poll: ({
22
33
  resultsField,
@@ -1,9 +1,13 @@
1
1
  "use client";
2
+ import { keys } from "../translations/keys.js";
3
+ import { isPollClosed } from "../form/pollState.js";
4
+ import { en } from "../translations/en.js";
5
+ import { makeTranslate } from "../translations/makeTranslate.js";
2
6
  import { Form } from "./Form.js";
3
7
  import { FormResults } from "./FormResults.js";
4
8
  import { fetchFormResults } from "./fetchResults.js";
5
- import { useCallback, useEffect, useState } from "react";
6
- import { jsx } from "react/jsx-runtime";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { useCallback, useEffect, useMemo, useState } from "react";
7
11
  //#region src/react/Poll.tsx
8
12
  const readVoted = (key) => {
9
13
  try {
@@ -18,15 +22,27 @@ const writeVoted = (key) => {
18
22
  } catch {}
19
23
  };
20
24
  /**
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
+ * A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows
26
+ * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated
27
+ * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open
28
+ * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A
29
+ * recorded `outcome.winningValue` (via `resolvePollOutcome`) supersedes everything: a translated final
30
+ * notice plus results with the winning bucket highlighted. A
31
+ * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`
32
+ * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not
33
+ * integrity (bypassable): server-enforced
34
+ * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.
25
35
  */
26
36
  const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchFormResults, apiRoute, onSuccess, ...formProps }) => {
27
37
  const key = storageKey ?? `fb-poll-${formProps.form.id}`;
38
+ const poll = formProps.form.poll;
39
+ const closed = isPollClosed(poll);
40
+ const winningValue = poll?.outcome?.winningValue;
41
+ const finalized = typeof winningValue === "string" && winningValue.length > 0;
42
+ const resultsAwaitClose = !closed && !finalized && poll?.resultsVisibility === "afterClose";
28
43
  const [voted, setVoted] = useState(false);
29
44
  const [results, setResults] = useState(null);
45
+ const translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t]);
30
46
  const loadResults = useCallback(async () => {
31
47
  const result = await fetchResultsImpl({
32
48
  formId: formProps.form.id,
@@ -41,30 +57,62 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
41
57
  apiRoute
42
58
  ]);
43
59
  useEffect(() => {
44
- if (hasVoted ?? readVoted(key)) {
45
- setVoted(true);
46
- loadResults();
47
- }
60
+ const already = hasVoted === true || readVoted(key);
61
+ if (already) setVoted(true);
62
+ if (already && !resultsAwaitClose || closed || finalized) loadResults();
48
63
  }, [
49
64
  hasVoted,
50
65
  key,
51
- loadResults
66
+ loadResults,
67
+ closed,
68
+ resultsAwaitClose,
69
+ finalized
52
70
  ]);
53
71
  const handleSuccess = useCallback((submissionId) => {
54
72
  writeVoted(key);
55
73
  setVoted(true);
56
- loadResults();
74
+ if (!resultsAwaitClose) loadResults();
57
75
  onSuccess?.(submissionId);
58
76
  }, [
59
77
  key,
60
78
  loadResults,
61
- onSuccess
79
+ onSuccess,
80
+ resultsAwaitClose
62
81
  ]);
63
- if (voted) return results ? /* @__PURE__ */ jsx(FormResults, {
64
- results,
65
- t: formProps.t,
66
- locale: formProps.locale
67
- }) : null;
82
+ if (finalized) return /* @__PURE__ */ jsxs("div", {
83
+ className: "fb-poll fb-poll--final",
84
+ children: [/* @__PURE__ */ jsx("p", {
85
+ className: "fb-poll__final",
86
+ children: translate(keys.pollFinalResult)
87
+ }), results ? /* @__PURE__ */ jsx(FormResults, {
88
+ results,
89
+ winningValue,
90
+ t: formProps.t,
91
+ locale: formProps.locale
92
+ }) : null]
93
+ });
94
+ if (closed) return /* @__PURE__ */ jsxs("div", {
95
+ className: "fb-poll fb-poll--closed",
96
+ children: [/* @__PURE__ */ jsx("p", {
97
+ className: "fb-poll__closed",
98
+ children: translate(keys.pollClosed)
99
+ }), results ? /* @__PURE__ */ jsx(FormResults, {
100
+ results,
101
+ t: formProps.t,
102
+ locale: formProps.locale
103
+ }) : null]
104
+ });
105
+ if (voted) {
106
+ if (resultsAwaitClose) return /* @__PURE__ */ jsx("p", {
107
+ className: "fb-poll__await-close",
108
+ children: translate(keys.pollResultsAfterClose)
109
+ });
110
+ return results ? /* @__PURE__ */ jsx(FormResults, {
111
+ results,
112
+ t: formProps.t,
113
+ locale: formProps.locale
114
+ }) : null;
115
+ }
68
116
  return /* @__PURE__ */ jsx(Form, {
69
117
  ...formProps,
70
118
  apiRoute,
@@ -1 +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"}
1
+ {"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { isPollClosed } from '../form/pollState'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\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 `poll.resultsField`). */\n\tresultsField: string\n\t/** localStorage key for the per-browser voted guard. Default `fb-poll-{form.id}`. */\n\tstorageKey?: string\n\t/**\n\t * Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;\n\t * `false`/omitted falls back to localStorage. SSR hosts using the plugin's `poll.votedCookie`\n\t * option pass `hasVotedCookie(cookieHeader, form.id)` here.\n\t */\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>` while open and not yet voted, then fetches the aggregate results and shows\n * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated\n * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open\n * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A\n * recorded `outcome.winningValue` (via `resolvePollOutcome`) supersedes everything: a translated final\n * notice plus results with the winning bucket highlighted. A\n * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`\n * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not\n * integrity (bypassable): server-enforced\n * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.\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 poll = formProps.form.poll\n\tconst closed = isPollClosed(poll)\n\tconst winningValue = poll?.outcome?.winningValue\n\tconst finalized = typeof winningValue === 'string' && winningValue.length > 0\n\tconst resultsAwaitClose = !closed && !finalized && poll?.resultsVisibility === 'afterClose'\n\tconst [voted, setVoted] = useState(false)\n\tconst [results, setResults] = useState<FieldAggregation[] | null>(null)\n\tconst translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t])\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 === true || readVoted(key)\n\t\tif (already) {\n\t\t\tsetVoted(true)\n\t\t}\n\t\tif ((already && !resultsAwaitClose) || closed || finalized) {\n\t\t\tvoid loadResults()\n\t\t}\n\t}, [hasVoted, key, loadResults, closed, resultsAwaitClose, finalized])\n\n\tconst handleSuccess = useCallback(\n\t\t(submissionId?: string) => {\n\t\t\twriteVoted(key)\n\t\t\tsetVoted(true)\n\t\t\tif (!resultsAwaitClose) {\n\t\t\t\tvoid loadResults()\n\t\t\t}\n\t\t\tonSuccess?.(submissionId)\n\t\t},\n\t\t[key, loadResults, onSuccess, resultsAwaitClose]\n\t)\n\n\tif (finalized) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--final\">\n\t\t\t\t<p className=\"fb-poll__final\">{translate(keys.pollFinalResult)}</p>\n\t\t\t\t{results ? (\n\t\t\t\t\t<FormResults\n\t\t\t\t\t\tresults={results}\n\t\t\t\t\t\twinningValue={winningValue}\n\t\t\t\t\t\tt={formProps.t}\n\t\t\t\t\t\tlocale={formProps.locale}\n\t\t\t\t\t/>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (closed) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--closed\">\n\t\t\t\t<p className=\"fb-poll__closed\">{translate(keys.pollClosed)}</p>\n\t\t\t\t{results ? (\n\t\t\t\t\t<FormResults results={results} t={formProps.t} locale={formProps.locale} />\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (voted) {\n\t\tif (resultsAwaitClose) {\n\t\t\treturn <p className=\"fb-poll__await-close\">{translate(keys.pollResultsAfterClose)}</p>\n\t\t}\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":";;;;;;;;;;;AA8BA,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;;;;;;;;;;;;;AAcA,MAAa,QAAQ,EACpB,cACA,YACA,UACA,mBAAmB,kBACnB,UACA,WACA,GAAG,gBACa;CAChB,MAAM,MAAM,cAAc,WAAW,UAAU,KAAK;CACpD,MAAM,OAAO,UAAU,KAAK;CAC5B,MAAM,SAAS,aAAa,IAAI;CAChC,MAAM,eAAe,MAAM,SAAS;CACpC,MAAM,YAAY,OAAO,iBAAiB,YAAY,aAAa,SAAS;CAC5E,MAAM,oBAAoB,CAAC,UAAU,CAAC,aAAa,MAAM,sBAAsB;CAC/E,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,MAAM,CAAC,SAAS,cAAc,SAAoC,IAAI;CACtE,MAAM,YAAY,cAAc,UAAU,KAAK,cAAc,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;CAE/E,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;EACf,MAAM,UAAU,aAAa,QAAQ,UAAU,GAAG;EAClD,IAAI,SACH,SAAS,IAAI;EAEd,IAAK,WAAW,CAAC,qBAAsB,UAAU,WAChD,YAAiB;CAEnB,GAAG;EAAC;EAAU;EAAK;EAAa;EAAQ;EAAmB;CAAS,CAAC;CAErE,MAAM,gBAAgB,aACpB,iBAA0B;EAC1B,WAAW,GAAG;EACd,SAAS,IAAI;EACb,IAAI,CAAC,mBACJ,YAAiB;EAElB,YAAY,YAAY;CACzB,GACA;EAAC;EAAK;EAAa;EAAW;CAAiB,CAChD;CAEA,IAAI,WACH,OACC,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,KAAD;GAAG,WAAU;aAAkB,UAAU,KAAK,eAAe;EAAK,CAAA,GACjE,UACA,oBAAC,aAAD;GACU;GACK;GACd,GAAG,UAAU;GACb,QAAQ,UAAU;EAClB,CAAA,IACE,IACA;;CAIP,IAAI,QACH,OACC,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,KAAD;GAAG,WAAU;aAAmB,UAAU,KAAK,UAAU;EAAK,CAAA,GAC7D,UACA,oBAAC,aAAD;GAAsB;GAAS,GAAG,UAAU;GAAG,QAAQ,UAAU;EAAS,CAAA,IACvE,IACA;;CAIP,IAAI,OAAO;EACV,IAAI,mBACH,OAAO,oBAAC,KAAD;GAAG,WAAU;aAAwB,UAAU,KAAK,qBAAqB;EAAK,CAAA;EAEtF,OAAO,UACN,oBAAC,aAAD;GAAsB;GAAS,GAAG,UAAU;GAAG,QAAQ,UAAU;EAAS,CAAA,IACvE;CACL;CAEA,OAAO,oBAAC,MAAD;EAAM,GAAI;EAAqB;EAAU,WAAW;CAAgB,CAAA;AAC5E"}
@@ -0,0 +1,28 @@
1
+ import { CaptchaWidgetHandle } from "./types.js";
2
+ import { Ref } from "react";
3
+
4
+ //#region src/react/captcha/HcaptchaCaptcha.d.ts
5
+ declare const HCAPTCHA_SCRIPT_URL = "https://js.hcaptcha.com/1/api.js?render=explicit";
6
+ type HcaptchaCaptchaProps = {
7
+ /** hCaptcha site key (public). */siteKey: string; /** Receives each fresh token, and null when the token expires or errors. */
8
+ onToken: (token: string | null) => void; /** Extra vendor render params (theme, size, ...). */
9
+ options?: Record<string, unknown>;
10
+ className?: string; /** Override the vendor script URL. */
11
+ scriptSrc?: string;
12
+ ref?: Ref<CaptchaWidgetHandle>;
13
+ };
14
+ /**
15
+ * Headless hCaptcha widget: loads the vendor script once, renders the widget into a bare div,
16
+ * and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.
17
+ */
18
+ declare const HcaptchaCaptcha: ({
19
+ siteKey,
20
+ onToken,
21
+ options,
22
+ className,
23
+ scriptSrc,
24
+ ref
25
+ }: HcaptchaCaptchaProps) => import("react/jsx-runtime").JSX.Element;
26
+ //#endregion
27
+ export { HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha, HcaptchaCaptchaProps };
28
+ //# sourceMappingURL=HcaptchaCaptcha.d.ts.map
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { useCaptchaScript } from "./useCaptchaScript.js";
3
+ import { getHcaptcha } from "./vendors.js";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { useEffect, useImperativeHandle, useRef } from "react";
6
+ //#region src/react/captcha/HcaptchaCaptcha.tsx
7
+ const HCAPTCHA_SCRIPT_URL = "https://js.hcaptcha.com/1/api.js?render=explicit";
8
+ /**
9
+ * Headless hCaptcha widget: loads the vendor script once, renders the widget into a bare div,
10
+ * and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.
11
+ */
12
+ const HcaptchaCaptcha = ({ siteKey, onToken, options, className, scriptSrc = HCAPTCHA_SCRIPT_URL, ref }) => {
13
+ const containerRef = useRef(null);
14
+ const widgetIdRef = useRef(null);
15
+ const onTokenRef = useRef(onToken);
16
+ onTokenRef.current = onToken;
17
+ const optionsRef = useRef(options);
18
+ optionsRef.current = options;
19
+ const ready = useCaptchaScript(scriptSrc);
20
+ useImperativeHandle(ref, () => ({ reset: () => {
21
+ const api = getHcaptcha();
22
+ if (api && widgetIdRef.current !== null) api.reset(widgetIdRef.current);
23
+ onTokenRef.current(null);
24
+ } }), []);
25
+ useEffect(() => {
26
+ if (!ready) return;
27
+ const container = containerRef.current;
28
+ const api = getHcaptcha();
29
+ if (!container || !api) return;
30
+ const widgetId = api.render(container, {
31
+ ...optionsRef.current,
32
+ sitekey: siteKey,
33
+ callback: (token) => onTokenRef.current(token),
34
+ "expired-callback": () => onTokenRef.current(null),
35
+ "error-callback": () => onTokenRef.current(null)
36
+ });
37
+ widgetIdRef.current = widgetId;
38
+ return () => {
39
+ widgetIdRef.current = null;
40
+ try {
41
+ api.remove(widgetId);
42
+ } catch {}
43
+ };
44
+ }, [ready, siteKey]);
45
+ return /* @__PURE__ */ jsx("div", {
46
+ ref: containerRef,
47
+ className
48
+ });
49
+ };
50
+ //#endregion
51
+ export { HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha };
52
+
53
+ //# sourceMappingURL=HcaptchaCaptcha.js.map