adp-web-components 0.3.18 → 0.3.20

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 (364) hide show
  1. package/dist/cjs/{branch-slot-dropdown_15.cjs.entry.js → branch-date-dropdown_17.cjs.entry.js} +958 -68
  2. package/dist/cjs/dead-stock-lookup_3.cjs.entry.js +9 -9
  3. package/dist/cjs/flexible-container.cjs.entry.js +3 -3
  4. package/dist/cjs/form-checkbox.cjs.entry.js +2 -2
  5. package/dist/cjs/form-date-picker.cjs.entry.js +5 -5
  6. package/dist/cjs/form-input_7.cjs.entry.js +21 -19
  7. package/dist/cjs/form-switch.cjs.entry.js +2 -2
  8. package/dist/cjs/form-time-picker.cjs.entry.js +6 -6
  9. package/dist/cjs/{functions-C9XsBzrL.js → functions-BAUJUMt-.js} +44 -19
  10. package/dist/cjs/general-form.cjs.entry.js +6 -6
  11. package/dist/cjs/general-inquiry-form.cjs.entry.js +6 -6
  12. package/dist/cjs/{get-local-language-BnUadlVY.js → get-local-language-DI_UK6D5.js} +1 -1
  13. package/dist/cjs/{get-mock-files-Dr5dpq1N.js → get-mock-files-B1tNGb9m.js} +1 -1
  14. package/dist/cjs/information-table.cjs.entry.js +1 -1
  15. package/dist/cjs/loader.cjs.js +1 -1
  16. package/dist/cjs/loading-spinner.cjs.entry.js +2 -2
  17. package/dist/cjs/manufacturer-part-lookup.cjs.entry.js +3 -3
  18. package/dist/cjs/{mappers-BlH-TAo7.js → mappers-DnWgECCv.js} +54 -18
  19. package/dist/cjs/part-lookup.cjs.entry.js +2 -2
  20. package/dist/cjs/service-booking-form.cjs.entry.js +6 -6
  21. package/dist/cjs/shift-accordion.cjs.entry.js +2 -2
  22. package/dist/cjs/shift-checkbox.cjs.entry.js +2 -2
  23. package/dist/cjs/shift-components.cjs.js +1 -1
  24. package/dist/cjs/shift-slider.cjs.entry.js +1 -1
  25. package/dist/cjs/shift-switch.cjs.entry.js +2 -2
  26. package/dist/cjs/shift-tab-content.cjs.entry.js +2 -2
  27. package/dist/cjs/shift-tabs_2.cjs.entry.js +92 -34
  28. package/dist/cjs/ssc-lookup-form.cjs.entry.js +6 -6
  29. package/dist/cjs/test-drive-form.cjs.entry.js +6 -6
  30. package/dist/cjs/triangle-alert-CTvU1eGY.js +248 -0
  31. package/dist/cjs/vehicle-accessories_8.cjs.entry.js +177 -117
  32. package/dist/cjs/vehicle-lookup.cjs.entry.js +3 -3
  33. package/dist/cjs/vehicle-quotation-form.cjs.entry.js +17 -18
  34. package/dist/collection/collection-manifest.json +2 -0
  35. package/dist/collection/components/components/empty.css +1 -1
  36. package/dist/collection/components/components/flexible-container.css +1 -1
  37. package/dist/collection/components/components/flexible-container.js +2 -2
  38. package/dist/collection/components/components/information-table.css +1 -1
  39. package/dist/collection/components/components/loading-spinner.css +1 -1
  40. package/dist/collection/components/components/loading-spinner.js +1 -1
  41. package/dist/collection/components/components/shift-accordion.css +1 -1
  42. package/dist/collection/components/components/shift-accordion.js +1 -1
  43. package/dist/collection/components/components/shift-checkbox.js +1 -1
  44. package/dist/collection/components/components/shift-portal.css +1 -1
  45. package/dist/collection/components/components/shift-select-dropdown.css +1 -1
  46. package/dist/collection/components/components/shift-select-dropdown.js +2 -2
  47. package/dist/collection/components/components/shift-select.css +1 -1
  48. package/dist/collection/components/components/shift-select.js +3 -3
  49. package/dist/collection/components/components/shift-slider.js +1 -1
  50. package/dist/collection/components/components/shift-switch.js +1 -1
  51. package/dist/collection/components/components/shift-tab-content.css +1 -1
  52. package/dist/collection/components/components/shift-tab-content.js +1 -1
  53. package/dist/collection/components/components/shift-tabs.css +1 -1
  54. package/dist/collection/components/components/shift-tabs.js +2 -2
  55. package/dist/collection/components/form-elements/branch-date-dropdown.css +1 -0
  56. package/dist/collection/components/form-elements/branch-date-dropdown.js +682 -0
  57. package/dist/collection/components/form-elements/branch-date-picker.css +1 -0
  58. package/dist/collection/components/form-elements/branch-date-picker.js +1197 -0
  59. package/dist/collection/components/form-elements/branch-slot-dropdown.css +1 -1
  60. package/dist/collection/components/form-elements/branch-slot-dropdown.js +11 -6
  61. package/dist/collection/components/form-elements/branch-slot-picker.css +1 -1
  62. package/dist/collection/components/form-elements/branch-slot-picker.js +166 -18
  63. package/dist/collection/components/form-elements/form-checkbox.js +1 -1
  64. package/dist/collection/components/form-elements/form-date-picker-legacy.js +4 -4
  65. package/dist/collection/components/form-elements/form-dialog.css +1 -1
  66. package/dist/collection/components/form-elements/form-dialog.js +1 -1
  67. package/dist/collection/components/form-elements/form-file.js +2 -2
  68. package/dist/collection/components/form-elements/form-input.js +6 -4
  69. package/dist/collection/components/form-elements/form-inputs.css +1 -1
  70. package/dist/collection/components/form-elements/form-phone-number.js +4 -2
  71. package/dist/collection/components/form-elements/form-picker-input.js +6 -4
  72. package/dist/collection/components/form-elements/form-shadow-input.js +1 -1
  73. package/dist/collection/components/form-elements/form-stepper-control.js +2 -2
  74. package/dist/collection/components/form-elements/form-stepper-submit.js +2 -2
  75. package/dist/collection/components/form-elements/form-stepper.js +1 -1
  76. package/dist/collection/components/form-elements/form-structure-error.js +1 -1
  77. package/dist/collection/components/form-elements/form-submit.js +2 -2
  78. package/dist/collection/components/form-elements/form-switch.js +1 -1
  79. package/dist/collection/components/form-elements/form-text-area.js +6 -4
  80. package/dist/collection/components/form-elements/form-time-picker-legacy.js +5 -5
  81. package/dist/collection/components/form-elements/form-vin-input.js +6 -4
  82. package/dist/collection/components/forms/defaults/mappers.js +54 -18
  83. package/dist/collection/components/forms/defaults/style.css +1 -1
  84. package/dist/collection/components/forms/general-form.js +2 -2
  85. package/dist/collection/components/forms/general-inquiry.js +2 -2
  86. package/dist/collection/components/forms/service-booking.js +2 -2
  87. package/dist/collection/components/forms/ssc-lookup.js +2 -2
  88. package/dist/collection/components/forms/test-drive.js +2 -2
  89. package/dist/collection/components/forms/vehicle-quotation/themes.css +1 -1
  90. package/dist/collection/components/forms/vehicle-quotation.js +15 -16
  91. package/dist/collection/components/part-lookup/dead-stock-lookup.css +1 -1
  92. package/dist/collection/components/part-lookup/dead-stock-lookup.js +2 -2
  93. package/dist/collection/components/part-lookup/distributor-lookup.css +1 -1
  94. package/dist/collection/components/part-lookup/distributor-lookup.js +2 -2
  95. package/dist/collection/components/part-lookup/manufacturer-lookup.css +1 -1
  96. package/dist/collection/components/part-lookup/manufacturer-part-lookup.css +1 -1
  97. package/dist/collection/components/part-lookup/manufacturer-part-lookup.js +1 -1
  98. package/dist/collection/components/part-lookup/part-lookup.css +1 -1
  99. package/dist/collection/components/vehicle-lookup/components/claimable-item-popover.js +37 -29
  100. package/dist/collection/components/vehicle-lookup/components/claimable-trace-modal.js +6 -2
  101. package/dist/collection/components/vehicle-lookup/components/service-history-sub-row.js +3 -3
  102. package/dist/collection/components/vehicle-lookup/vehicle-accessories.css +1 -1
  103. package/dist/collection/components/vehicle-lookup/vehicle-accessories.js +2 -2
  104. package/dist/collection/components/vehicle-lookup/vehicle-claimable-items.css +1 -1
  105. package/dist/collection/components/vehicle-lookup/vehicle-claimable-items.js +121 -70
  106. package/dist/collection/components/vehicle-lookup/vehicle-item-claim-form.css +1 -1
  107. package/dist/collection/components/vehicle-lookup/vehicle-item-claim-form.js +89 -29
  108. package/dist/collection/components/vehicle-lookup/vehicle-lookup.css +1 -1
  109. package/dist/collection/components/vehicle-lookup/vehicle-paint-thickness.css +1 -1
  110. package/dist/collection/components/vehicle-lookup/vehicle-sale-information.css +1 -1
  111. package/dist/collection/components/vehicle-lookup/vehicle-sale-information.js +2 -2
  112. package/dist/collection/components/vehicle-lookup/vehicle-service-history.css +1 -1
  113. package/dist/collection/components/vehicle-lookup/vehicle-service-history.js +1 -1
  114. package/dist/collection/components/vehicle-lookup/vehicle-specification.css +1 -1
  115. package/dist/collection/components/vehicle-lookup/vehicle-specification.js +1 -1
  116. package/dist/collection/components/vehicle-lookup/vehicle-warranty-details.css +1 -1
  117. package/dist/collection/components/vehicle-lookup/vehicle-warranty-timeline.css +1 -1
  118. package/dist/collection/components/vehicle-lookup/vehicle-warranty-timeline.js +1 -1
  119. package/dist/collection/components/vin-extractor/vin-extractor.css +1 -1
  120. package/dist/collection/components/vin-extractor/vin-extractor.js +1 -1
  121. package/dist/collection/features/form-hook/functions.js +42 -18
  122. package/dist/collection/global/lib/overlay.js +154 -0
  123. package/dist/collection/global/lib/slot-day-rules.js +65 -0
  124. package/dist/collection/templates/assets/adp-wordmark.svg +1 -0
  125. package/dist/collection/templates/assets/favicon.svg +17 -0
  126. package/dist/collection/templates/forms/docs-content/params.js +1199 -0
  127. package/dist/collection/templates/forms/docs-content/structure.js +1153 -0
  128. package/dist/collection/templates/forms/docs-content/theming.js +1240 -0
  129. package/dist/collection/templates/harness-theme.js +44 -0
  130. package/dist/collection/templates/harness.js +775 -0
  131. package/dist/collection/templates/highlight.js +194 -0
  132. package/dist/collection/templates/nav.js +320 -0
  133. package/dist/collection/templates/site-header.js +642 -0
  134. package/dist/collection/templates/site-locales.js +771 -0
  135. package/dist/components/branch-date-dropdown.d.ts +11 -0
  136. package/dist/components/branch-date-dropdown.js +5 -0
  137. package/dist/components/branch-date-picker.d.ts +11 -0
  138. package/dist/components/branch-date-picker.js +5 -0
  139. package/dist/components/branch-slot-dropdown.js +1 -1
  140. package/dist/components/branch-slot-picker.js +1 -1
  141. package/dist/components/dead-stock-lookup.js +1 -1
  142. package/dist/components/distributor-lookup.js +1 -1
  143. package/dist/components/flexible-container.js +1 -1
  144. package/dist/components/form-checkbox.js +1 -1
  145. package/dist/components/form-date-picker.js +1 -1
  146. package/dist/components/form-dialog.js +1 -1
  147. package/dist/components/form-file.js +1 -1
  148. package/dist/components/form-input-preview.js +1 -1
  149. package/dist/components/form-input.js +1 -1
  150. package/dist/components/form-phone-number.js +1 -1
  151. package/dist/components/form-picker-input.js +1 -1
  152. package/dist/components/form-select.js +1 -1
  153. package/dist/components/form-shadow-input.js +1 -1
  154. package/dist/components/form-stepper-control.js +1 -1
  155. package/dist/components/form-stepper-submit.js +1 -1
  156. package/dist/components/form-stepper.js +1 -1
  157. package/dist/components/form-structure-error.js +1 -1
  158. package/dist/components/form-structure.js +1 -1
  159. package/dist/components/form-submit.js +1 -1
  160. package/dist/components/form-switch.js +1 -1
  161. package/dist/components/form-text-area.js +1 -1
  162. package/dist/components/form-time-picker.js +1 -1
  163. package/dist/components/form-vin-input.js +1 -1
  164. package/dist/components/general-form.js +1 -1
  165. package/dist/components/general-inquiry-form.js +1 -1
  166. package/dist/components/information-table.js +1 -1
  167. package/dist/components/loading-spinner.js +1 -1
  168. package/dist/components/manufacturer-lookup.js +1 -1
  169. package/dist/components/manufacturer-part-lookup.js +1 -1
  170. package/dist/components/p--lk4OUJg.js +5 -0
  171. package/dist/components/p-38ox9qxm.js +5 -0
  172. package/dist/components/p-5OD_mwiD.js +5 -0
  173. package/dist/components/{p-D7wRHBb3.js → p-68bjv9gT.js} +1 -1
  174. package/dist/components/p-B0_5_VvS.js +5 -0
  175. package/dist/components/p-B1xVBTqj.js +5 -0
  176. package/dist/components/{p-B0lK-5c_.js → p-B2bD-55K.js} +1 -1
  177. package/dist/components/{p-Bs5AIefE.js → p-BI5NL1Rh.js} +1 -1
  178. package/dist/components/{p-Be01Kv65.js → p-BPXRokES.js} +1 -1
  179. package/dist/components/{p-oZMLGlUq.js → p-BULuujHY.js} +1 -1
  180. package/dist/components/{p-B6EgLMus.js → p-BWlX-Nz-.js} +1 -1
  181. package/dist/components/p-Bc5HIbcj.js +5 -0
  182. package/dist/components/p-BcWJAP8j.js +5 -0
  183. package/dist/components/p-BpB_86n0.js +5 -0
  184. package/dist/components/p-BpHkd1L9.js +5 -0
  185. package/dist/components/{p-B0tcNl8W.js → p-BsnGFZeT.js} +1 -1
  186. package/dist/components/{p-B_jDD5xI.js → p-BvLVCp-j.js} +1 -1
  187. package/dist/components/p-C3KLOUbk.js +5 -0
  188. package/dist/components/{p-DgJ9CCPI.js → p-CJtTHErI.js} +1 -1
  189. package/dist/components/{p-DcB3e3j1.js → p-CMAmzdM7.js} +1 -1
  190. package/dist/components/p-CVpbr1tR.js +5 -0
  191. package/dist/components/{p-BVdlf8rx.js → p-CXbi5iPZ.js} +1 -1
  192. package/dist/components/p-C_1_eN4N.js +5 -0
  193. package/dist/components/{p-TtDq4UV5.js → p-Ce2mazYo.js} +1 -1
  194. package/dist/components/p-Cg6Prz81.js +5 -0
  195. package/dist/components/{p-l738xIxs.js → p-Cpjvk1pk.js} +1 -1
  196. package/dist/components/p-CqJiXtPb.js +5 -0
  197. package/dist/components/p-CrPyvAdX.js +5 -0
  198. package/dist/components/{p-C31A3MHr.js → p-CsU_c7eX.js} +1 -1
  199. package/dist/components/{p-Ci6k5mSb.js → p-D42YuMNZ.js} +1 -1
  200. package/dist/components/{p-Dhi7g1w3.js → p-DCpRGVqe.js} +1 -1
  201. package/dist/components/{p-D9pKibY5.js → p-DCsMDWbS.js} +1 -1
  202. package/dist/components/{p-BMnQwtNn.js → p-DQ0uTCi2.js} +1 -1
  203. package/dist/components/p-DTmI_Yx0.js +5 -0
  204. package/dist/components/p-DnspXuVN.js +5 -0
  205. package/dist/components/p-DqSdjh2n.js +5 -0
  206. package/dist/components/p-Dv2j7fhS.js +5 -0
  207. package/dist/components/{p-BUczNscD.js → p-K-OF4DG-.js} +1 -1
  208. package/dist/components/p-KSvRVGk-.js +5 -0
  209. package/dist/components/p-KbiYvxzP.js +5 -0
  210. package/dist/components/{p-DG-IgRFW.js → p-OY4Ixaeq.js} +1 -1
  211. package/dist/components/{p-BiaT4YFg.js → p-SIS25ChD.js} +1 -1
  212. package/dist/components/p-Wgb4y0H3.js +5 -0
  213. package/dist/components/p-X3AzJ0KD.js +5 -0
  214. package/dist/components/{p-gzdpekMI.js → p-bCeQwUKM.js} +1 -1
  215. package/dist/components/p-d3ZIPeEC.js +5 -0
  216. package/dist/components/{p-DlqfD7uh.js → p-ikaUQvOO.js} +1 -1
  217. package/dist/components/p-mbVTvSJF.js +5 -0
  218. package/dist/components/{p-DTdm5I7a.js → p-rQ3Z9EnS.js} +1 -1
  219. package/dist/components/{p-BmQrqnnJ.js → p-uYkn8a0m.js} +1 -1
  220. package/dist/components/part-lookup.js +1 -1
  221. package/dist/components/service-booking-form.js +1 -1
  222. package/dist/components/shift-accordion.js +1 -1
  223. package/dist/components/shift-checkbox.js +1 -1
  224. package/dist/components/shift-portal.js +1 -1
  225. package/dist/components/shift-select-dropdown.js +1 -1
  226. package/dist/components/shift-select.js +1 -1
  227. package/dist/components/shift-slider.js +1 -1
  228. package/dist/components/shift-switch.js +1 -1
  229. package/dist/components/shift-tab-content.js +1 -1
  230. package/dist/components/shift-tabs.js +1 -1
  231. package/dist/components/ssc-lookup-form.js +1 -1
  232. package/dist/components/test-drive-form.js +1 -1
  233. package/dist/components/vehicle-accessories.js +1 -1
  234. package/dist/components/vehicle-claimable-items.js +1 -1
  235. package/dist/components/vehicle-item-claim-form.js +1 -1
  236. package/dist/components/vehicle-lookup.js +1 -1
  237. package/dist/components/vehicle-paint-thickness.js +1 -1
  238. package/dist/components/vehicle-quotation-form.js +1 -1
  239. package/dist/components/vehicle-sale-information.js +1 -1
  240. package/dist/components/vehicle-service-history.js +1 -1
  241. package/dist/components/vehicle-specification.js +1 -1
  242. package/dist/components/vehicle-warranty-details.js +1 -1
  243. package/dist/components/vehicle-warranty-timeline.js +1 -1
  244. package/dist/components/vin-extractor.js +1 -1
  245. package/dist/esm/{branch-slot-dropdown_15.entry.js → branch-date-dropdown_17.entry.js} +957 -69
  246. package/dist/esm/dead-stock-lookup_3.entry.js +9 -9
  247. package/dist/esm/flexible-container.entry.js +3 -3
  248. package/dist/esm/form-checkbox.entry.js +2 -2
  249. package/dist/esm/form-date-picker.entry.js +5 -5
  250. package/dist/esm/form-input_7.entry.js +21 -19
  251. package/dist/esm/form-switch.entry.js +2 -2
  252. package/dist/esm/form-time-picker.entry.js +6 -6
  253. package/dist/esm/{functions-COLiDjnd.js → functions-DSwc1YVL.js} +44 -20
  254. package/dist/esm/general-form.entry.js +6 -6
  255. package/dist/esm/general-inquiry-form.entry.js +6 -6
  256. package/dist/esm/{get-local-language-CACck9dv.js → get-local-language-DdSAPSCV.js} +1 -1
  257. package/dist/esm/{get-mock-files-BEIYZlS_.js → get-mock-files-D4naDetb.js} +1 -1
  258. package/dist/esm/information-table.entry.js +1 -1
  259. package/dist/esm/loader.js +1 -1
  260. package/dist/esm/loading-spinner.entry.js +2 -2
  261. package/dist/esm/manufacturer-part-lookup.entry.js +3 -3
  262. package/dist/esm/{mappers-9IKbvPiR.js → mappers-CZs2R7k6.js} +54 -18
  263. package/dist/esm/part-lookup.entry.js +2 -2
  264. package/dist/esm/service-booking-form.entry.js +6 -6
  265. package/dist/esm/shift-accordion.entry.js +2 -2
  266. package/dist/esm/shift-checkbox.entry.js +2 -2
  267. package/dist/esm/shift-components.js +1 -1
  268. package/dist/esm/shift-slider.entry.js +1 -1
  269. package/dist/esm/shift-switch.entry.js +2 -2
  270. package/dist/esm/shift-tab-content.entry.js +2 -2
  271. package/dist/esm/shift-tabs_2.entry.js +92 -34
  272. package/dist/esm/ssc-lookup-form.entry.js +6 -6
  273. package/dist/esm/test-drive-form.entry.js +6 -6
  274. package/dist/esm/triangle-alert-BlnfPzWh.js +237 -0
  275. package/dist/esm/vehicle-accessories_8.entry.js +177 -117
  276. package/dist/esm/vehicle-lookup.entry.js +3 -3
  277. package/dist/esm/vehicle-quotation-form.entry.js +17 -18
  278. package/dist/integration-manifest.json +1 -1
  279. package/dist/locales/vehicleLookup/serviceHistory/en.json +2 -2
  280. package/dist/shift-components/{p-a91ed0fc.entry.js → p-0b576888.entry.js} +1 -1
  281. package/dist/shift-components/p-11ffa908.entry.js +5 -0
  282. package/dist/shift-components/{p-b2048d87.entry.js → p-152057de.entry.js} +1 -1
  283. package/dist/shift-components/p-198333fa.entry.js +5 -0
  284. package/dist/shift-components/{p-537507ee.entry.js → p-247ac2db.entry.js} +1 -1
  285. package/dist/shift-components/{p-1923fdb9.entry.js → p-295e445f.entry.js} +1 -1
  286. package/dist/shift-components/p-431522f2.entry.js +5 -0
  287. package/dist/shift-components/{p-b1270d98.entry.js → p-49dc6d73.entry.js} +1 -1
  288. package/dist/shift-components/{p-ac91d4ce.entry.js → p-4b6f801a.entry.js} +1 -1
  289. package/dist/shift-components/{p-6263bc0c.entry.js → p-4cdeab63.entry.js} +1 -1
  290. package/dist/shift-components/{p-829f24b9.entry.js → p-52530fba.entry.js} +1 -1
  291. package/dist/shift-components/p-5ad4d072.entry.js +5 -0
  292. package/dist/shift-components/{p-a0d13479.entry.js → p-5e8e5ed7.entry.js} +1 -1
  293. package/dist/shift-components/{p-65aad39b.entry.js → p-68e41217.entry.js} +1 -1
  294. package/dist/shift-components/{p-f2b86186.entry.js → p-76b0ea8c.entry.js} +1 -1
  295. package/dist/shift-components/{p-fdf94f65.entry.js → p-85d43a41.entry.js} +1 -1
  296. package/dist/shift-components/p-B9INqdFT.js +5 -0
  297. package/dist/shift-components/p-BkVfhjgg.js +5 -0
  298. package/dist/shift-components/{p--JyU3LyU.js → p-D2Ofzg_i.js} +1 -1
  299. package/dist/shift-components/p-DDtDRGFL.js +5 -0
  300. package/dist/shift-components/{p-jmNTa9tM.js → p-OfPuHUYc.js} +1 -1
  301. package/dist/shift-components/p-b44b2c00.entry.js +5 -0
  302. package/dist/shift-components/{p-48a9feb9.entry.js → p-b4544d10.entry.js} +1 -1
  303. package/dist/shift-components/{p-853f1b8b.entry.js → p-bfe0661c.entry.js} +1 -1
  304. package/dist/shift-components/{p-e7a99998.entry.js → p-c7fc8d5f.entry.js} +1 -1
  305. package/dist/shift-components/{p-bb172fc4.entry.js → p-ca0683e9.entry.js} +1 -1
  306. package/dist/shift-components/{p-cfcfdb20.entry.js → p-cbdc1c2e.entry.js} +1 -1
  307. package/dist/shift-components/p-d3bbba07.entry.js +5 -0
  308. package/dist/shift-components/{p-ec2a7d65.entry.js → p-df0c7ac1.entry.js} +1 -1
  309. package/dist/shift-components/p-f4679148.entry.js +5 -0
  310. package/dist/shift-components/{p-c4cf0a18.entry.js → p-feb443ec.entry.js} +1 -1
  311. package/dist/shift-components/shift-components.esm.js +1 -1
  312. package/dist/stencil-docs.json +1677 -12
  313. package/dist/templates/production-host/vehicle-service-history.html +14 -0
  314. package/dist/types/components/form-elements/branch-date-dropdown.d.ts +74 -0
  315. package/dist/types/components/form-elements/branch-date-picker.d.ts +174 -0
  316. package/dist/types/components/form-elements/branch-slot-dropdown.d.ts +6 -0
  317. package/dist/types/components/form-elements/branch-slot-picker.d.ts +27 -0
  318. package/dist/types/components/forms/defaults/mappers.d.ts +15 -9
  319. package/dist/types/components/vehicle-lookup/components/claimable-item-popover.d.ts +10 -1
  320. package/dist/types/components/vehicle-lookup/components/claimable-trace-modal.d.ts +3 -0
  321. package/dist/types/components/vehicle-lookup/vehicle-claimable-items.d.ts +14 -4
  322. package/dist/types/components/vehicle-lookup/vehicle-item-claim-form.d.ts +23 -3
  323. package/dist/types/components.d.ts +533 -6
  324. package/dist/types/features/form-hook/functions.d.ts +14 -0
  325. package/dist/types/global/lib/overlay.d.ts +61 -0
  326. package/dist/types/global/lib/slot-day-rules.d.ts +25 -0
  327. package/dist/types/locales/vehicleLookup/serviceHistory/type.d.ts +2 -2
  328. package/dist/types/locales/vehicleLookup/warrantyTimeline/type.d.ts +4 -4
  329. package/dist/types/locales/vehicleLookup/wrapper-type.d.ts +2 -2
  330. package/package.json +15 -3
  331. package/dist/cjs/triangle-alert-BjQBwsRv.js +0 -92
  332. package/dist/components/p-BQHywBX6.js +0 -5
  333. package/dist/components/p-Bb4mauUH.js +0 -5
  334. package/dist/components/p-Bl_BMBad.js +0 -5
  335. package/dist/components/p-BogCz_yJ.js +0 -5
  336. package/dist/components/p-BvFDjUZZ.js +0 -5
  337. package/dist/components/p-C2trAVtF.js +0 -5
  338. package/dist/components/p-CCuYl9VQ.js +0 -5
  339. package/dist/components/p-CNqcMKLv.js +0 -5
  340. package/dist/components/p-CZWmJ1vV.js +0 -5
  341. package/dist/components/p-Cg4l7ICV.js +0 -5
  342. package/dist/components/p-Ci6LbIzY.js +0 -5
  343. package/dist/components/p-CuTemmgW.js +0 -5
  344. package/dist/components/p-D0oXezbg.js +0 -5
  345. package/dist/components/p-DBKG_o1-.js +0 -5
  346. package/dist/components/p-Dlm9wHfQ.js +0 -5
  347. package/dist/components/p-DsMy6H9e.js +0 -5
  348. package/dist/components/p-Dseg250d.js +0 -5
  349. package/dist/components/p-EVz84xxv.js +0 -5
  350. package/dist/components/p-SnXklPq9.js +0 -5
  351. package/dist/components/p-k8G1hnHC.js +0 -5
  352. package/dist/components/p-uPiGNvhQ.js +0 -5
  353. package/dist/esm/triangle-alert-Brtqhx2d.js +0 -86
  354. package/dist/shift-components/p-34dffc00.entry.js +0 -5
  355. package/dist/shift-components/p-6fa1121c.entry.js +0 -5
  356. package/dist/shift-components/p-782fe0e1.entry.js +0 -5
  357. package/dist/shift-components/p-9a6450f6.entry.js +0 -5
  358. package/dist/shift-components/p-B9kqtQjS.js +0 -5
  359. package/dist/shift-components/p-D0Md1Xf7.js +0 -5
  360. package/dist/shift-components/p-c66b18c9.entry.js +0 -5
  361. package/dist/shift-components/p-c9a7e5ab.entry.js +0 -5
  362. package/dist/shift-components/p-fa7a961c.entry.js +0 -5
  363. package/dist/shift-components/p-hABirfUj.js +0 -5
  364. /package/dist/collection/templates/{vehicle-lookup → prototypes}/prototype-assets/extended-warranty/adp-logo.svg +0 -0
@@ -0,0 +1,1153 @@
1
+ /**
2
+ * Structure-JSON reference for the ticket-form components.
3
+ *
4
+ * Plain data. No DOM, no framework, no imports — a documentation page renders
5
+ * this; nothing here runs inside a form.
6
+ *
7
+ * Every claim below was read out of the source, not remembered:
8
+ * src/features/form-hook/interface.ts the structure type
9
+ * src/features/form-hook/render-structure.tsx the whole authoring contract
10
+ * src/features/form-hook/functions.ts load, reCAPTCHA, submit, UTM
11
+ * src/features/form-hook/form-hook.ts validation, steps, required
12
+ * src/components/forms/defaults/mappers.tsx the field-name table
13
+ * src/components/forms/defaults/validation.ts the base yup schema
14
+ *
15
+ * The five components that share this format are `general-form`,
16
+ * `general-inquiry-form`, `service-booking-form`, `ssc-lookup-form` and
17
+ * `test-drive-form`. `vehicle-quotation-form` reads a structure that looks the
18
+ * same but is wired to its own mapper table and its own submit function, so
19
+ * nothing on this page is guaranteed to hold there.
20
+ *
21
+ * Examples are template literals holding strictly valid JSON, so a page can
22
+ * print them verbatim or `JSON.parse` them.
23
+ */
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Top level
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export const topLevel = [
30
+ {
31
+ key: 'data',
32
+ type: 'object',
33
+ required: false,
34
+ summary:
35
+ 'Everything about the form that is not a rendered node: where to POST, which headers to add, the reCAPTCHA key, the payload rewrites, the localisation bundle. Nothing under `data` renders. It is read straight off `structure.data` by the component and by `onFormSubmit` — `renderStructure` destructures `data` away and never looks at it, so a `data` key on a *child* node is silently discarded.',
36
+ example: `{
37
+ "data": {
38
+ "requestUrl": "https://example.invalid/api/tickets",
39
+ "requestMethod": "POST",
40
+ "theme": "docs-preset",
41
+ "localization": {
42
+ "en": { "submit": "Send", "Form submitted successfully.": "Thanks - we will be in touch." }
43
+ }
44
+ },
45
+ "tag": "div",
46
+ "children": [{ "name": "name" }, { "name": "submit" }]
47
+ }`,
48
+ absent:
49
+ 'The form still renders. It fails at submit: `requestEndpoint` is empty, so `onFormSubmit` throws `Request endpoint is not configured`, which surfaces in the error dialog. Until v-current this was worse — `Object.hasOwn(undefined, …)` threw during load and the form sat on its spinner forever; `resolveIsMobileForm` now defaults `data` to `{}`.',
50
+ },
51
+
52
+ {
53
+ key: 'requiredContext',
54
+ type: 'Record<string, boolean>',
55
+ required: false,
56
+ summary:
57
+ 'The only structure-level required switch. `FormHook.getRequiredContext()` turns every entry into a yup context variable by appending `Required` — `{"email": true}` becomes `$emailRequired` — and the base schema branches on exactly those names via `.when("$<field>Required", …)`. It is read once, in the `FormHook` constructor.',
58
+ example: `{
59
+ "requiredContext": { "name": true, "email": true, "message": true, "companyBranchId": true }
60
+ }`,
61
+ absent:
62
+ 'Every base-schema field is optional. `.when` falls to its `otherwise` branch, which is `schema.optional()` for all of them. A field name that is not in the base schema (phone, file, bookingSlot, bookingDate, or any name you invented with `type`) ignores this key completely — see `requiredMechanics.exceptions`.',
63
+ },
64
+
65
+ {
66
+ key: 'steps',
67
+ type: 'Array<Record<"en" | "ar" | "ku" | "ru", Step>>',
68
+ required: false,
69
+ summary:
70
+ 'Presence of this array is what turns the form into a wizard. `form-structure` then renders the tree once as a chrome pass (`currentStep === -1`) plus once per entry (`currentStep === 1 … n`), and `FormHook.submitForm` advances a step instead of POSTing until `steps.length === currentStep`. Array order is step order; the array is indexed `steps[currentStep - 1]`.',
71
+ example: `{
72
+ "steps": [
73
+ {
74
+ "en": { "title": "Details", "stepCell": "1", "stepTitle": "Your details", "submitButton": "Continue" },
75
+ "ar": { "title": "البيانات", "stepCell": "١", "stepTitle": "بياناتك", "submitButton": "متابعة" }
76
+ },
77
+ {
78
+ "en": { "title": "Vehicle", "stepCell": "2", "stepTitle": "Your vehicle", "submitButton": "Book", "back": "Back" },
79
+ "ar": { "title": "المركبة", "stepCell": "٢", "stepTitle": "مركبتك", "submitButton": "احجز", "back": "رجوع" }
80
+ }
81
+ ]
82
+ }`,
83
+ absent:
84
+ 'Single-pass form. `form-structure` calls `renderStructure(…, -2)` once, and `submitForm` POSTs immediately. In that single pass a node carrying a truthy `step` is dropped, because `-2` never equals it — so leaving `step` on a node after deleting `steps` makes the node vanish.',
85
+ },
86
+
87
+ {
88
+ key: 'tag',
89
+ type: 'string',
90
+ required: true,
91
+ summary:
92
+ 'The root is a node like any other, so it needs `tag` (an HTML tag name) or `name`. In practice it is always `tag`, because the root is the wrapper everything else hangs inside. A `name`-only root gives you a single field and no wrapper.',
93
+ example: `{
94
+ "tag": "div",
95
+ "id": "container",
96
+ "children": []
97
+ }`,
98
+ absent:
99
+ 'With neither `tag` nor `name` (and no `type`), `renderStructure` returns `false` and the entire form renders as nothing — no error, no console warning. See `nodeProps` → `tag`.',
100
+ },
101
+
102
+ {
103
+ key: 'id',
104
+ type: 'string',
105
+ required: false,
106
+ summary:
107
+ 'On a tag node this becomes the literal DOM `id` and is also folded into the element `part`, which is how a host stylesheet reaches inside the shadow root: `::part(container)`.',
108
+ example: `{
109
+ "tag": "div",
110
+ "id": "container",
111
+ "children": [{ "tag": "div", "id": "inputs_wrapper", "children": [{ "name": "name" }] }]
112
+ }`,
113
+ absent: 'No id attribute; `part` is built from the remaining pieces (`class`, `element-<tag>`, `<tag>`).',
114
+ },
115
+
116
+ {
117
+ key: 'class',
118
+ type: 'string',
119
+ required: false,
120
+ summary: 'On a tag node this becomes the DOM `class` and is also folded into `part`. On a field node it arrives as `wrapperClass` instead — see `nodeProps` → `class`.',
121
+ example: `{
122
+ "tag": "div",
123
+ "id": "container",
124
+ "class": "form-grid",
125
+ "children": [{ "tag": "h2", "class": "section-title", "children": { "en": "Contact us", "ar": "اتصل بنا" } }]
126
+ }`,
127
+ absent: 'No class attribute.',
128
+ },
129
+
130
+ {
131
+ key: 'children',
132
+ type: 'Array<node | string> | Record<LanguageKeys, string>',
133
+ required: false,
134
+ summary:
135
+ 'On a tag node, either an array of child nodes (each recursed through `renderStructure`) or a plain object keyed by language, which renders as localised text — that is how you get a heading. A string entry in the array is shorthand for `{ "name": "<string>" }`. On a field node `children` is destructured away and ignored.',
136
+ example: `{
137
+ "tag": "div",
138
+ "children": [
139
+ { "tag": "h2", "children": { "en": "Contact us", "ar": "اتصل بنا", "ku": "پەیوەندیمان پێوە بکە", "ru": "Свяжитесь с нами" } },
140
+ { "name": "name" },
141
+ "submit"
142
+ ]
143
+ }`,
144
+ absent: 'An empty element. A root with no `children` renders an empty form with a hidden submit button and nothing else.',
145
+ },
146
+ ];
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // structure.data.*
150
+ // ---------------------------------------------------------------------------
151
+
152
+ export const dataKeys = [
153
+ {
154
+ key: 'requestUrl',
155
+ type: 'string',
156
+ summary: 'Where a browser form POSTs. Used whenever the form is not in mobile mode.',
157
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets" } }`,
158
+ absent: 'Submit throws `Request endpoint is not configured` and the error dialog opens. Everything before that — validation, step advance, file uploads — still runs.',
159
+ seeAlso: ['requestMethod', 'isMobileForm', 'requestAppUrl', 'requestAppCheckUrl'],
160
+ },
161
+
162
+ {
163
+ key: 'requestMethod',
164
+ type: 'string',
165
+ summary:
166
+ 'HTTP verb. Two separate checks read it and they do not agree: UTM values move into the headers when the verb is `get` OR `head`, but the body is omitted only when the verb is `get`. So `"requestMethod": "HEAD"` still builds a body, and `fetch` then rejects the call outright — a HEAD request cannot carry one. `GET` is the only bodyless verb this code actually supports.',
167
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets", "requestMethod": "POST" } }`,
168
+ absent: 'Defaults to `POST`, in both the header branch and the `fetch` call.',
169
+ seeAlso: ['requestUrl', 'utm'],
170
+ },
171
+
172
+ {
173
+ key: 'requestAppUrl',
174
+ type: 'string',
175
+ summary: 'Mobile-mode endpoint used when `getMobileToken()` returns a value starting (case-insensitively) with `bearer`. The token is sent as `Authorization`.',
176
+ example: `{ "data": { "isMobileForm": true, "requestAppUrl": "https://example.invalid/api/app/tickets" } }`,
177
+ absent: 'In mobile mode with a bearer token, the endpoint is empty and submit throws `Request endpoint is not configured`.',
178
+ seeAlso: ['isMobileForm', 'requestAppCheckUrl'],
179
+ },
180
+
181
+ {
182
+ key: 'requestAppCheckUrl',
183
+ type: 'string',
184
+ summary: 'The other mobile-mode endpoint — used when the token does not start with `bearer`. The token is then sent as a `verification-token` header instead.',
185
+ example: `{ "data": { "isMobileForm": true, "requestAppCheckUrl": "https://example.invalid/api/appcheck/tickets" } }`,
186
+ absent: 'Same failure as above, for the non-bearer branch.',
187
+ seeAlso: ['isMobileForm', 'requestAppUrl'],
188
+ },
189
+
190
+ {
191
+ key: 'isMobileForm',
192
+ type: 'boolean',
193
+ summary:
194
+ 'Switches the whole submit path: no reCAPTCHA script is injected at load, no reCAPTCHA token at submit, and the endpoint comes from `requestAppUrl` / `requestAppCheckUrl`. The check is `Object.hasOwn(data, "isMobileForm")`, not truthiness, so `"isMobileForm": false` in the structure deliberately overrides a `is-mobile-form` attribute set to true on the element.',
195
+ example: `{ "data": { "isMobileForm": false, "requestUrl": "https://example.invalid/api/tickets" } }`,
196
+ absent: 'Falls back to the component prop `isMobileForm`, which defaults to `false`.',
197
+ seeAlso: ['recaptchaKey', 'requestAppUrl', 'requestAppCheckUrl'],
198
+ },
199
+
200
+ {
201
+ key: 'recaptchaKey',
202
+ type: 'string',
203
+ summary:
204
+ 'Site key for reCAPTCHA v3. When present and not in mobile mode, `formDidLoadHandler` appends `https://www.google.com/recaptcha/api.js?render=<key>&hl=<language>` to `document.head`, and submit calls `grecaptcha.execute(key, { action: "submit" })` and sends the result as a `Recaptcha-Token` header. `form-file` does the same before asking for its signed-upload URLs.',
205
+ example: `{
206
+ "data": {
207
+ "requestUrl": "https://example.invalid/api/tickets",
208
+ "requestMethod": "POST"
209
+ }
210
+ }`,
211
+ absent:
212
+ 'THE DOCUMENTED DEFAULT, and what every example on this page does. No script is injected, no token header is sent, and the form submits. Do not paste a placeholder key to "show the shape": the page looks healthy right up to Submit, where `grecaptcha.execute` rejects with `Invalid site key` and the failure reads like a network error.',
213
+ seeAlso: ['isMobileForm', 'wireFormat'],
214
+ },
215
+
216
+ {
217
+ key: 'brandId',
218
+ type: 'string',
219
+ summary: 'Sent verbatim as the `Brand` request header on every submit.',
220
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets", "brandId": "demo-brand" } }`,
221
+ absent:
222
+ 'The header is still set — to the string `"undefined"`, because the header object is built unconditionally and `Headers` stringifies the value. If your backend rejects an unknown brand, an omitted `brandId` fails as a bad brand rather than a missing one.',
223
+ seeAlso: ['wireFormat'],
224
+ },
225
+
226
+ {
227
+ key: 'extraHeader',
228
+ type: 'object',
229
+ summary: 'Merged into the request headers last, after the component `extraHeader` prop. Structure wins over prop.',
230
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets", "extraHeader": { "X-Source": "docs-site" } } }`,
231
+ absent: 'Only the built-in headers plus whatever the `extraHeader` prop carries.',
232
+ seeAlso: ['extraPayload', 'wireFormat'],
233
+ },
234
+
235
+ {
236
+ key: 'extraPayload',
237
+ type: 'object',
238
+ summary:
239
+ 'Merged into the JSON body — and here the precedence is the other way round: `data.extraPayload` is applied first, then the component `extraPayload` prop, so the PROP wins. Headers and payload disagree about which side is authoritative; that asymmetry is in the source, not a typo here.',
240
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets", "extraPayload": { "source": "web", "campaignId": 42 } } }`,
241
+ absent: 'The body is the validated form values plus UTM values plus `additionalData` if `truncatedFields` is set.',
242
+ seeAlso: ['extraHeader', 'truncatedFields', 'wireFormat'],
243
+ },
244
+
245
+ {
246
+ key: 'truncatedFields',
247
+ type: 'Record<string, string | string[]>',
248
+ summary:
249
+ 'A tiny rewrite language applied to the payload just before it is sent, in object-key order. Four behaviours, chosen by the shape of each entry:\n' +
250
+ '1. value is an ARRAY — join. Each item is looked up in the payload; if there is no such key the item is used as a literal. The result is assigned to the entry key. Source fields are NOT removed.\n' +
251
+ '2. key starts with `parse date: ` — `payload[rest] = parse(payload[rest], <value>, new Date())` using the date-fns format in the value. Produces a Date object.\n' +
252
+ '3. key starts with `format date: ` — `payload[rest] = formatISO(payload[rest])`. The value is ignored; any non-empty string works, so write something self-documenting.\n' +
253
+ '4. anything else — RENAME AND MOVE: `additionalData[<value>] = payload[<key>]`, then `delete payload[<key>]`.\n' +
254
+ 'Whenever this key is present and non-empty, `payload.additionalData` is set, even if it ends up `{}`.',
255
+ example: `{
256
+ "data": {
257
+ "requestUrl": "https://example.invalid/api/tickets",
258
+ "truncatedFields": {
259
+ "preferredDateTime": ["date", " ", "time"],
260
+ "parse date: preferredDateTime": "yyyy-MM-dd HH:mm",
261
+ "format date: preferredDateTime": "iso",
262
+ "message": "customerNotes"
263
+ }
264
+ }
265
+ }`,
266
+ absent: 'The payload is sent as validated. No `additionalData` key is added at all.',
267
+ seeAlso: ['extraPayload', 'wireFormat'],
268
+ },
269
+
270
+ {
271
+ key: 'disableUTMLog',
272
+ type: 'boolean',
273
+ summary: 'Truthy value makes `getMarketingValues` return `{}`, so no marketing parameter is collected or sent.',
274
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets", "disableUTMLog": true } }`,
275
+ absent: 'Marketing parameters present in the page URL are collected and sent.',
276
+ seeAlso: ['utm'],
277
+ },
278
+
279
+ {
280
+ key: 'localization',
281
+ type: 'Record<LanguageKeys, object>',
282
+ summary:
283
+ 'The form-wide locale bundle. On every language change the block for the new language is picked (falling back to `.en`, then `{}`) and becomes the component `locale`, with its optional `sharedFormLocales` sub-object layered over the shipped shared locales. Two well-known keys: `submit` (the default submit-button label, via `submitTextKey`) and `"Form submitted successfully."` (the success dialog text). Per-field text does NOT live here — it lives in each node\'s own `localization`.',
284
+ example: `{
285
+ "data": {
286
+ "requestUrl": "https://example.invalid/api/tickets",
287
+ "localization": {
288
+ "en": {
289
+ "submit": "Send",
290
+ "Form submitted successfully.": "Thanks - we will be in touch.",
291
+ "sharedFormLocales": { "close": "Dismiss" }
292
+ },
293
+ "ar": { "submit": "إرسال", "Form submitted successfully.": "تم الإرسال بنجاح." }
294
+ }
295
+ }
296
+ }`,
297
+ absent: 'The shipped shared locales are used throughout; the submit button falls back to `sharedFormLocales.submit`, then to the literal `Submit`.',
298
+ seeAlso: ['nodeProps → localization'],
299
+ },
300
+
301
+ {
302
+ key: 'theme',
303
+ type: 'string',
304
+ summary:
305
+ 'Appended to the outer wrapper `part`, alongside the component `theme` prop: `part="shift-form <data.theme> <theme prop>"`. It is a styling hook, nothing more — no stylesheet is loaded for you.',
306
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets", "theme": "docs-preset" } }`,
307
+ absent: 'The wrapper part is just `shift-form` plus the `theme` prop if one was set.',
308
+ seeAlso: [],
309
+ },
310
+
311
+ {
312
+ key: 'currentVehiclesApi',
313
+ type: 'string',
314
+ summary:
315
+ 'Endpoint for the `currentVehicleBrand` field only. It is the one field mapper that reaches into `structure.data` instead of taking a node prop — and it reads `structure?.data.currentVehiclesApi`, with no `?.` after `data`, so a form that uses this field and omits `data` entirely throws inside the fetcher.',
316
+ example: `{
317
+ "data": {
318
+ "requestUrl": "https://example.invalid/api/tickets",
319
+ "currentVehiclesApi": "https://example.invalid/api/vehicle-brands"
320
+ },
321
+ "tag": "div",
322
+ "children": [{ "name": "currentVehicleBrand" }, { "name": "currentVehicleModel" }]
323
+ }`,
324
+ absent: '`fetch(undefined)` — the select shows a fetch error and stays empty.',
325
+ seeAlso: ['mappers → currentVehicleBrand', 'mappers → conditionalCurrentVehicleBrand'],
326
+ },
327
+ ];
328
+
329
+ // ---------------------------------------------------------------------------
330
+ // Node properties
331
+ // ---------------------------------------------------------------------------
332
+
333
+ export const nodeProps = [
334
+ {
335
+ prop: 'tag',
336
+ appliesTo: 'container nodes',
337
+ summary:
338
+ 'Renders a raw element of that name and recurses into `children`. The element gets `part={cn(id, class, "element-<tag>", "<tag>")}`. A `tag` node is never a form field: `name` is destructured but unused on this branch, and no mapper is consulted.',
339
+ example: `{ "tag": "section", "id": "contact", "class": "panel", "children": [{ "name": "name" }] }`,
340
+ absent: 'The node is treated as a field node and looked up in the mapper table by `type || name`.',
341
+ warning:
342
+ 'TRAP 1. A node with neither `tag` nor `name` — and no `type` — renders NOTHING and says nothing. `renderStructure` falls off the end and returns `false`. The same silence covers a `name`/`type` that is not a key in the mapper table: a typo like `"emial"` produces an empty gap, no console warning, no error panel. `part` is assigned after the prop spread, so a `part` you write on the node is overwritten.',
343
+ },
344
+
345
+ {
346
+ prop: 'name',
347
+ appliesTo: 'field nodes',
348
+ summary:
349
+ 'Two jobs at once. It is the mapper key (unless `type` overrides it) and it is the field name — the `name` attribute on the input, the key in the submitted payload, the key in the yup schema, and the key `requiredContext` and `truncatedFields` refer to.',
350
+ example: `{ "name": "email", "localization": { "en": { "label": "Email", "placeholder": "you@example.invalid" } } }`,
351
+ absent: 'Only legal on a `tag` node. On a field node without `type`, nothing renders.',
352
+ warning:
353
+ 'A string in a `children` array is shorthand for this: `"submit"` and `{ "name": "submit" }` reach the same mapper. The shorthand cannot carry `id`, `class`, `step` or `localization`.',
354
+ },
355
+
356
+ {
357
+ prop: 'type',
358
+ appliesTo: 'field nodes',
359
+ summary:
360
+ 'Overrides the mapper lookup while leaving `name` as the field name: `elementMapper[type || name]`. This is the ONLY way to give a form a field name the base schema has never heard of — reuse a shipped widget under a new name.',
361
+ example: `{
362
+ "name": "secondaryEmail",
363
+ "type": "email",
364
+ "localization": { "en": { "label": "Second email", "placeholder": "you@example.invalid" } }
365
+ }`,
366
+ absent: 'The mapper key is `name`.',
367
+ warning:
368
+ 'TRAP 7 (the useful half). A new field NAME is fine — the value is collected by `getValues` and reaches the wire. A new field TYPE is impossible: `getFormMappers()` is called once at module scope in each form component, stored in a `const`, and passed down as a prop the host cannot set. There is no registry, no `extraMappers` prop, no custom-element escape hatch. A name invented this way is also unvalidated and permanently starred — see `requiredMechanics.exceptions`. Note also that `type` is destructured out by `renderStructure` and never forwarded, so you cannot use it to set an `<input type>`; use `inputProps` for that.',
369
+ },
370
+
371
+ {
372
+ prop: 'id',
373
+ appliesTo: 'both',
374
+ summary:
375
+ 'On a tag node: the DOM id, plus a `part` token. On a field node: forwarded as `wrapperId`, which each input puts on its `<label>` wrapper — and, because the raw `id` is *also* still in the forwarded props, on the component host element as well.',
376
+ example: `{ "name": "message", "id": "message_field", "class": "wide" }`,
377
+ absent: 'No id anywhere; `part` is built from what remains.',
378
+ warning: 'Ids are not de-duplicated. A node without a `step` in a stepped form is rendered once per pass, so its id is emitted n+1 times.',
379
+ },
380
+
381
+ {
382
+ prop: 'class',
383
+ appliesTo: 'both',
384
+ summary:
385
+ 'On a tag node: the DOM class, plus a `part` token. On a field node: forwarded as `wrapperClass` onto the `<label>` wrapper — and, like `id`, also left in the raw props, so it lands on the host element too.',
386
+ example: `{ "tag": "div", "id": "inputs_wrapper", "class": "grid two-up", "children": [{ "name": "name" }, { "name": "lastName" }] }`,
387
+ absent: 'No class.',
388
+ warning: null,
389
+ },
390
+
391
+ {
392
+ prop: 'step',
393
+ appliesTo: 'both',
394
+ summary:
395
+ 'Which render pass the node belongs to, 1-based. `-1` means the chrome pass — rendered once, outside the sliding step panels, which is where the stepper rail goes. The filter runs before anything else in `renderStructure`, so a filtered-out node never reaches the mapper and its children are never walked.',
396
+ example: `{
397
+ "steps": [{ "en": { "title": "One", "stepCell": "1", "submitButton": "Continue" } }, { "en": { "title": "Two", "stepCell": "2", "submitButton": "Book", "back": "Back" } }],
398
+ "tag": "div",
399
+ "children": [
400
+ { "name": "formStepper", "step": -1 },
401
+ { "tag": "div", "step": 1, "children": [{ "name": "name" }, { "name": "stepperSubmit", "step": 1 }] },
402
+ { "tag": "div", "step": 2, "children": [{ "name": "email" }, { "name": "back", "step": 2 }, { "name": "stepperSubmit", "step": 2 }] }
403
+ ]
404
+ }`,
405
+ absent:
406
+ 'TRAP 4. In a SINGLE-STEP form, absent is correct and required. In a STEPPED form, a node with no `step` renders in the chrome pass AND in every step pass — n+1 copies, with n+1 identical `name` attributes, n+1 identical ids, and n+1 `form.subscribe()` calls under the same key. `getValues` then keeps whichever copy the DOM walk sees last, and a single `unsubscribe` drops them all. Give every node under a stepped root a `step`.',
407
+ warning:
408
+ 'Two more edge cases fall out of the same guard. `step: 0` is falsy, so it behaves like "no step" in the step passes but IS excluded from the chrome pass. And `step` is not destructured away — it stays in the forwarded props, so a tag node ships a literal `step="1"` attribute into the DOM.',
409
+ },
410
+
411
+ {
412
+ prop: 'staticValue',
413
+ appliesTo: 'field nodes',
414
+ summary:
415
+ 'Pins a value and disables the control (`isDisabled` is computed as `… || !!this.staticValue`). A disabled input is deliberately still read by `getValues`, which special-cases `el.disabled` because `FormData` would skip it — so the pinned value does reach the payload.',
416
+ example: `{ "name": "email", "staticValue": "prefilled@example.invalid" }`,
417
+ absent: 'A normal editable field.',
418
+ warning:
419
+ 'On a select-backed field this is an OPTION object, not a string — `{ "value": …, "label": … }`, optionally keyed by language — and it replaces the fetcher entirely, so no request is made:\n' +
420
+ '{ "name": "companyBranchId", "staticValue": { "value": "12", "label": "Central branch" } }',
421
+ },
422
+
423
+ {
424
+ prop: 'isHidden',
425
+ appliesTo: 'field nodes',
426
+ summary:
427
+ 'Intended to mean "present but invisible". It half works. `FormHook.hasItemInStructure` treats an `isHidden` node as absent, and `submitForm` omits every schema field that `hasItemInStructure` cannot find — so the field is dropped from submit validation. Its VALUE is still collected and still sent, because yup keeps unknown keys.',
428
+ example: `{ "name": "companyBranchId", "isHidden": true, "staticValue": { "value": "12", "label": "Central branch" }, "branchApi": "https://example.invalid/api/branches" }`,
429
+ absent: 'The field renders and validates normally.',
430
+ warning:
431
+ 'TRAP 3, and the single most surprising thing in this format. `isHidden` is a declared prop on exactly ONE component: `form-select`. So on a select-backed field (vehicle, companyBranchId, vacancyId, cityId, time, year, ownVehicle, currentVehicleBrand, currentVehicleModel, conditionalCurrentVehicleBrand, conditionalCurrentVehicleModel, generalTicketType) it renders `display: none` around a `form-shadow-input` that carries the value — genuinely hidden, still submitted. On EVERY OTHER field — text inputs, textarea, phone, file, date/time pickers, VIN, the branch pickers — nothing reads the prop. The field stays fully visible and fully interactive; the only effect is that it stops being validated. It reaches the DOM as an inert `ishidden` attribute.',
432
+ },
433
+
434
+ {
435
+ prop: 'isDisabled',
436
+ appliesTo: 'field nodes',
437
+ summary:
438
+ 'Greys the control out. Read by form-input, form-text-area, form-phone-number, form-file, form-picker-input, form-vin-input and form-select. The value is still submitted (see `staticValue`).',
439
+ example: `{ "name": "message", "isDisabled": true }`,
440
+ absent: 'Enabled, unless the form is loading or a `staticValue` is pinned.',
441
+ warning:
442
+ 'Ignored on `bookingSlot` and `bookingDate`: their mappers spread your props first and then set `isDisabled={!hasBranch}` last, so the picker is disabled exactly while no branch is selected and your value is discarded. Also ignored by the buttons (`submit`, `stepperSubmit`, `back`), which disable on `isLoading` only.',
443
+ },
444
+
445
+ {
446
+ prop: 'required',
447
+ appliesTo: 'field nodes (file, phone)',
448
+ summary:
449
+ 'A component-level prop, not a structure concept. `form-file` reads it and adds a "at least one file" test to its own schema; `form-phone-number` reads it in its own require test. Nothing else looks at it.',
450
+ example: `{
451
+ "name": "file",
452
+ "required": true,
453
+ "accept": "application/pdf",
454
+ "maxUpload": 1,
455
+ "maxSize": 5,
456
+ "signUrl": "https://example.invalid/api/sign",
457
+ "localization": { "en": { "label": "CV", "upload": "Upload your CV", "size": "File must be under 5 MB", "max": "One file only" } }
458
+ }`,
459
+ absent: 'The file field is optional (`.optional()`); the phone field defers entirely to whatever `getInputState` decided — which, in practice, is "required". See trap 5.',
460
+ warning: 'Writing `"required": true` on a base-schema field such as `email` does NOTHING. That knob is `requiredContext`.',
461
+ },
462
+
463
+ {
464
+ prop: 'defaultValue',
465
+ appliesTo: 'field nodes',
466
+ summary: 'Initial value, and the value `reset()` returns to after a successful submit. On a select it is matched against the fetched options and selects the matching one.',
467
+ example: `{ "name": "year", "min": 2015, "max": 2026, "defaultValue": "2024" }`,
468
+ absent: 'Empty field. On `vehicle`, an absent `defaultValue` lets the URL query parameter named by `vehicleIdQueryParam` preselect an option instead.',
469
+ warning: null,
470
+ },
471
+
472
+ {
473
+ prop: 'children',
474
+ appliesTo: 'container nodes',
475
+ summary: 'Array of child nodes and/or name strings, OR an object keyed by language for localised text. Both forms are handled on the `tag` branch only.',
476
+ example: `{ "tag": "p", "class": "hint", "children": { "en": "We reply within one working day.", "ar": "نرد خلال يوم عمل واحد." } }`,
477
+ absent: 'An empty element.',
478
+ warning: 'On a field node `children` is destructured away and dropped. Fields have no children.',
479
+ },
480
+
481
+ {
482
+ prop: 'localization',
483
+ appliesTo: 'field nodes',
484
+ summary:
485
+ 'Per-field text, keyed by language. `label` and `placeholder` are read on every input. The error keys are matched by SUFFIX against the yup message, which is always `<field>-<kind>`: `require`, `format`, `size`, and `failure` (for the `-upload` message). `max` is read by the file field. The date/time picker adds `minMessage`, `maxMessage` and `betweenMessage`, which support the `$minDate$` and `$maxDate$` slots.',
486
+ example: `{
487
+ "name": "date",
488
+ "min": [0, 0, 1],
489
+ "max": [0, 1, 0],
490
+ "localization": {
491
+ "en": {
492
+ "label": "Date",
493
+ "placeholder": "Pick a date",
494
+ "require": "Date is required.",
495
+ "format": "That date is not valid.",
496
+ "minMessage": "Earliest available is $minDate$",
497
+ "maxMessage": "Latest available is $maxDate$"
498
+ },
499
+ "ar": { "label": "التاريخ", "placeholder": "اختر تاريخاً", "require": "التاريخ مطلوب." }
500
+ }
501
+ }`,
502
+ absent:
503
+ 'Text falls back to the shipped locale entry named by the schema `meta` (`<field>-label`, `<field>-placeholder`), and finally to that raw key string, which is what a missing translation looks like on screen.',
504
+ warning:
505
+ 'There is no fallback BETWEEN languages here. Unlike `data.localization`, which falls back to `en`, a node `localization` block is indexed by the active language only — so a node with only an `en` block shows the raw locale keys in Arabic.',
506
+ },
507
+ ];
508
+
509
+ // ---------------------------------------------------------------------------
510
+ // The mapper table
511
+ // ---------------------------------------------------------------------------
512
+
513
+ export const mappers = [
514
+ {
515
+ name: 'submit',
516
+ renders: 'form-submit',
517
+ minimalNode: `{ "name": "submit", "localization": { "en": { "label": "Send" } } }`,
518
+ needs: [],
519
+ notes:
520
+ 'A `type="submit"` button. Label resolution, first hit wins: node `localization[lang].label` → `locale[submitTextKey]` (default key `submit`, i.e. `data.localization[lang].submit`) → `sharedFormLocales.submit` → the literal `Submit`. Works inside a stepped form too — it just cannot read a per-step label.',
521
+ },
522
+
523
+ {
524
+ name: 'stepperSubmit',
525
+ renders: 'form-stepper-submit',
526
+ minimalNode: `{ "name": "stepperSubmit", "step": 2 }`,
527
+ needs: ['structure.steps', 'step on the node'],
528
+ notes:
529
+ 'Identical button, one extra label source: `steps[step - 1][lang].submitButton`, inserted between the node localisation and the form-wide `submit` key. Its `step` does double duty — it decides which pass renders the button AND which step label it reads. Omit `step` and it falls back to the current step, which is usually right but not always during a transition.',
530
+ },
531
+
532
+ {
533
+ name: 'back',
534
+ renders: 'form-stepper-control',
535
+ minimalNode: `{ "name": "back", "step": 2 }`,
536
+ needs: ['structure.steps'],
537
+ notes:
538
+ 'Previous-step button; calls `form.updateStep(-1)`. Its label is `steps[currentStep - 1][lang].back` and there is no node-level override — leave `back` out of a step and the button renders with a chevron and no text.',
539
+ },
540
+
541
+ {
542
+ name: 'formStepper',
543
+ renders: 'form-stepper',
544
+ minimalNode: `{ "name": "formStepper", "step": -1 }`,
545
+ needs: ['structure.steps'],
546
+ notes:
547
+ 'The numbered rail. Reads `steps[i][lang].stepCell` for the circle and `stepTitle` for the caption under it, straight from `form.context.structure.steps` — so it always shows every step, regardless of where the node sits. Give it `step: -1` so it renders once in the chrome pass instead of once per panel.',
548
+ },
549
+
550
+ {
551
+ name: 'inputPreview',
552
+ renders: 'form-input-preview (which renders a read-only form-input)',
553
+ minimalNode: `{
554
+ "name": "inputPreview",
555
+ "props": { "name": "summaryLine" },
556
+ "localization": { "en": { "label": "Summary", "value": "\${name} - \${email}" } }
557
+ }`,
558
+ needs: ['props.name', 'localization[lang].value'],
559
+ notes:
560
+ 'A read-only echo of other fields. `localization[lang].value` is a template: every `${…}` is replaced with `form.getValue(name)` at render time. The nested `props` object is spread onto the inner `form-input`, and it is where the inner field NAME has to go — the outer `name` is consumed as the mapper key. It subscribes under the constant key `form-input-preview`, so two previews in one form collide.',
561
+ },
562
+
563
+ {
564
+ name: 'name',
565
+ renders: 'form-input',
566
+ minimalNode: `{ "name": "name", "localization": { "en": { "label": "Full name", "placeholder": "Full name", "require": "Full name is required." } } }`,
567
+ needs: [],
568
+ notes: 'Base schema: optional by default; with `requiredContext.name` it is required and must be at least 3 characters (message key `name-format`).',
569
+ },
570
+
571
+ {
572
+ name: 'lastName',
573
+ renders: 'form-input',
574
+ minimalNode: `{ "name": "lastName", "localization": { "en": { "label": "Surname", "require": "Surname is required." } } }`,
575
+ needs: [],
576
+ notes: 'Same rules as `name`: min 3 characters when required.',
577
+ },
578
+
579
+ {
580
+ name: 'email',
581
+ renders: 'form-input with type="email"',
582
+ minimalNode: `{ "name": "email", "localization": { "en": { "label": "Email", "placeholder": "you@example.invalid", "format": "Email format is invalid." } } }`,
583
+ needs: [],
584
+ notes: 'The `.email()` check is OUTSIDE the `.when()`, so format is enforced even when the field is optional — an optional email must be empty or valid.',
585
+ },
586
+
587
+ {
588
+ name: 'message',
589
+ renders: 'form-text-area',
590
+ minimalNode: `{ "name": "message", "localization": { "en": { "label": "Message", "format": "Message must be 10 characters or more." } } }`,
591
+ needs: [],
592
+ notes: 'Min 10 characters when required.',
593
+ },
594
+
595
+ {
596
+ name: 'phone',
597
+ renders: 'form-phone-number',
598
+ minimalNode: `{ "name": "phone", "countryCode": "IQ", "localization": { "en": { "label": "Phone", "placeholder": "Phone number", "require": "Phone number is required.", "format": "That number is not valid." } } }`,
599
+ needs: ['countryCode'],
600
+ notes:
601
+ 'Not in the base schema — it brings its own yup schema through `FormElement.validate()`, using libphonenumber. `countryCode` is a string, an array of ISO codes, or an array of `{ "code": "IQ" }`; with more than one entry a country selector appears and the initial pick comes from the browser timezone, then the browser locale, then the first entry. The submitted value is the prefixed, formatted number (`+964 …`). See trap 5 in `requiredMechanics.exceptions`.',
602
+ },
603
+
604
+ {
605
+ name: 'vin',
606
+ renders: 'form-vin-input',
607
+ minimalNode: `{ "name": "vin", "useOcr": true, "scannerIcon": "camera", "ocrEndpoint": "https://example.invalid/api/vin-ocr", "localization": { "en": { "label": "VIN", "format": "That VIN is not valid.", "scan": "Scan your VIN" } } }`,
608
+ needs: [],
609
+ notes:
610
+ 'The VIN check digit is only validated when `requiredContext.vin` is true — the `validateVin` test sits inside the required branch, unlike `email`, whose format check runs either way. `useOcr`, `readSticker`, `scannerIcon` (`""`, `"qr-code"`, `"camera"`) and `ocrEndpoint` enable the camera scanner.',
611
+ },
612
+
613
+ {
614
+ name: 'file',
615
+ renders: 'form-file',
616
+ minimalNode: `{
617
+ "name": "file",
618
+ "required": true,
619
+ "accept": "application/pdf,.doc,.docx",
620
+ "maxUpload": 1,
621
+ "maxSize": 5,
622
+ "signUrl": "https://example.invalid/api/sign",
623
+ "localization": { "en": { "label": "CV", "upload": "Upload your CV", "size": "File must be under 5 MB", "max": "One file only" } }
624
+ }`,
625
+ needs: ['signUrl (for deferred upload)'],
626
+ notes:
627
+ 'Not in the base schema; brings its own via `validate()`. `maxSize` IS IN MEGABYTES — the test computes `maxSize * 1024 * 1024`, so `5` means 5 MB and `5242880` means five terabytes. `accept` matches MIME types, `type/*` wildcards and `.ext` suffixes. `signUrl` turns on deferred upload: files are signed and uploaded during submit, before the record is created, and a failed or in-flight upload blocks the whole form even when the field is optional. `signMethod`, `signPrefix`, `accountName` and `containerName` shape that request.',
628
+ },
629
+
630
+ {
631
+ name: 'fileUploader',
632
+ renders: 'form-file',
633
+ minimalNode: `{ "name": "attachment", "type": "fileUploader", "required": true, "maxSize": 10, "signUrl": "https://example.invalid/api/sign" }`,
634
+ needs: ['signUrl (for deferred upload)'],
635
+ notes:
636
+ 'A second key pointing at the identical component. Its practical use is exactly the example above: a second file field under a different field name, reached with `type`.',
637
+ },
638
+
639
+ {
640
+ name: 'vehicle',
641
+ renders: 'form-select (searchable)',
642
+ minimalNode: `{
643
+ "name": "vehicle",
644
+ "vehicleApi": "https://example.invalid/api/models",
645
+ "dynamic": true,
646
+ "items": "data",
647
+ "item-value": "id",
648
+ "item-label": "title",
649
+ "localization": { "en": { "label": "Model", "placeholder": "Choose a model" } }
650
+ }`,
651
+ needs: ['vehicleApi'],
652
+ notes:
653
+ 'Three response shapes. With `dynamic: true` the response is mapped by `populateItems` using `items` (path to the array), `item-value` and `item-label` (paths within each item; each accepts an array of paths and takes the first non-null). With `vehiclesApiStrapiFormat: true` it expects `{ data: [{ attributes: { GradeName, Cover } }] }`. With neither it expects a flat array of `{ ID, Title, Image }`. `useNamedValue` submits the label instead of the id. `vehicleIdQueryParam` names a URL query parameter that preselects a matching option when no `defaultValue` is set. The chosen option keeps the whole record in `meta`, which is what `vehicleImage` draws from.',
654
+ },
655
+
656
+ {
657
+ name: 'vehicleImage',
658
+ renders: 'VehicleImageViewer (a functional component, not a field)',
659
+ minimalNode: `{ "name": "vehicleImage" }`,
660
+ needs: ['a vehicle field in the same form'],
661
+ notes:
662
+ 'Renders the selected vehicle image and nothing else — no name, no value, no validation. It watches `vehicle` and reads `form.context.vehicleList`, the option array `form-select` writes back after fetching, taking `meta.image`. Without a `vehicle` field it renders a collapsed empty container. Images are fetched, base64-cached in a module-level map and re-rendered, so they survive a step change.',
663
+ },
664
+
665
+ {
666
+ name: 'companyBranchId',
667
+ renders: 'form-select (clearable, searchable)',
668
+ minimalNode: `{ "name": "companyBranchId", "branchApi": "https://example.invalid/api/branches", "localization": { "en": { "label": "Branch", "placeholder": "Select a branch" } } }`,
669
+ needs: ['branchApi'],
670
+ notes:
671
+ 'Expects an array of `{ ID, Name, … }`; label is `Name`, value is `String(ID)`, and the ENTIRE record is kept in `meta`. That is not cosmetic: `bookingSlot` and `bookingDate` resolve their four calendar ids out of this record (`CompanyIntegrationId`, `IntegrationId`, `Departments[].IntegrationId`, `Brands[].IntegrationId`), so those fields do not work without this one.',
672
+ },
673
+
674
+ {
675
+ name: 'vacancyId',
676
+ renders: 'form-select (clearable, searchable)',
677
+ minimalNode: `{ "name": "vacancyId", "vacancyApi": "https://example.invalid/api/vacancies", "localization": { "en": { "label": "Position", "placeholder": "Choose a position" } } }`,
678
+ needs: ['vacancyApi'],
679
+ notes: 'Array of `{ ID, Title }`; label is `Title`, value is `String(ID)`, full record in `meta`.',
680
+ },
681
+
682
+ {
683
+ name: 'cityId',
684
+ renders: 'form-select (clearable, searchable)',
685
+ minimalNode: `{ "name": "cityId", "cityApi": "https://example.invalid/api/cities", "localization": { "en": { "label": "City", "placeholder": "Choose a city" } } }`,
686
+ needs: ['cityApi'],
687
+ notes: 'Array of `{ ID, Name }`; label is `Name`, value is `String(ID)`.',
688
+ },
689
+
690
+ {
691
+ name: 'date',
692
+ renders: 'form-picker-input with type="date"',
693
+ minimalNode: `{
694
+ "name": "date",
695
+ "min": [0, 0, 1],
696
+ "max": [0, 1, 0],
697
+ "localization": { "en": { "label": "Date", "placeholder": "Pick a date", "minMessage": "Earliest available is $minDate$", "maxMessage": "Latest available is $maxDate$" } }
698
+ }`,
699
+ needs: [],
700
+ notes:
701
+ '`min` and `max` are either literal date strings or OFFSET ARRAYS relative to the start of today, in the order `[years, months, days, hours, minutes, seconds]` — so `[0, 0, 1]` is tomorrow and `[0, 1, 0]` is one month out. They are layered onto the base schema through `partialValidation`, which adds a `min-date`, `max-date` or `between-date` test whose message key (`minMessage` / `maxMessage` / `betweenMessage`) you localise on the node. An optional `format` string (date-fns) reformats the DISPLAYED value and publishes it as the submitted value through the `<field>-format` back-channel.',
702
+ },
703
+
704
+ {
705
+ name: 'time',
706
+ renders: 'form-select (clearable)',
707
+ minimalNode: `{
708
+ "name": "time",
709
+ "min": [0, 0, 0, 9, 0],
710
+ "max": [0, 0, 0, 17, 0],
711
+ "span": [0, 0, 0, 0, 30],
712
+ "format": "HH:mm",
713
+ "localization": { "en": { "label": "Time", "placeholder": "Pick a time" } }
714
+ }`,
715
+ needs: ['min', 'max', 'span', 'format'],
716
+ notes:
717
+ 'A fixed ladder built in the browser — no request. All four props are mandatory and all three arrays use the same `[y, mo, d, h, m, s]` offset shape; the example is 09:00 to 17:00 every 30 minutes. Identical at every branch on every day, which is exactly the difference from `bookingSlot`. Miss one prop and the option list is silently empty.',
718
+ },
719
+
720
+ {
721
+ name: 'year',
722
+ renders: 'form-select (clearable)',
723
+ minimalNode: `{ "name": "year", "min": 2015, "max": 2026, "localization": { "en": { "label": "Model year", "placeholder": "Choose a year" } } }`,
724
+ needs: [],
725
+ notes:
726
+ 'Plain numeric range; `min` defaults to the current year minus 20 and `max` to the current year. `firstOption` and `lastOption` prepend/append an extra entry, and both their label AND their value are keyed by language: `{ "firstOption": { "label": { "en": "Older" }, "value": { "en": "older" } } }`.',
727
+ },
728
+
729
+ {
730
+ name: 'bookingSlot',
731
+ renders: 'branch-slot-picker (day strip + time grid)',
732
+ minimalNode: `{
733
+ "name": "bookingSlot",
734
+ "calendarApi": "https://example.invalid/api/calendar",
735
+ "daysAhead": 30,
736
+ "disabledWeekdays": [5, 6],
737
+ "localization": { "en": { "label": "Date & time", "placeholder": "Choose a slot" } }
738
+ }`,
739
+ needs: ['calendarApi', 'a companyBranchId field in the same form'],
740
+ notes:
741
+ 'The chosen slot is NOT sent with the form — the trigger input carries no `name`, so `FormHook.getValues` never reads it; listen for the `slotChange` event on the form element and send the value yourself through `extraPayload`. Otherwise it asks the selected branch what is actually open, instead of assuming. Company, branch, department and brand ids are resolved from the `companyBranchId` option `meta`; `departmentPreference` (default `["showroom"]`) picks among the branch departments, and explicit `departmentId` / `brandId` node props override the resolution. The field stays disabled until a branch is chosen. `disabledWeekdays` (`0` Sunday … `6` Saturday) and `disabledDates` (`YYYY-MM-DD`) accept an array or a comma string; matching days stay visible but unselectable. Submits `YYYY-MM-DDTHH:mm`. See trap 6.',
742
+ },
743
+
744
+ {
745
+ name: 'bookingDate',
746
+ renders: 'branch-date-picker (month calendar)',
747
+ minimalNode: `{
748
+ "name": "bookingDate",
749
+ "calendarApi": "https://example.invalid/api/calendar",
750
+ "daysAhead": 30,
751
+ "localization": { "en": { "label": "Date & time", "placeholder": "Choose a slot" } }
752
+ }`,
753
+ needs: ['calendarApi', 'a companyBranchId field in the same form'],
754
+ notes:
755
+ 'Never sent with the form, exactly like `bookingSlot` — read it from the `slotChange` event instead. The month-calendar layout is the only difference: same props, same data, same event payload. The two are interchangeable; pick whichever your audience reads faster. Same caveats as `bookingSlot`, including trap 6.',
756
+ },
757
+
758
+ {
759
+ name: 'ownVehicle',
760
+ renders: 'form-select',
761
+ minimalNode: `{ "name": "ownVehicle", "localization": { "en": { "label": "Do you own a vehicle?", "yes": "Yes", "no": "No" } } }`,
762
+ needs: ['localization[lang].yes', 'localization[lang].no'],
763
+ notes:
764
+ 'A two-option select whose values are the literal strings `yes` and `no`. The option LABELS come from `localization[lang].yes` / `.no` — not from anything shared — so a missing block gives you two blank rows. Four other fields branch on this value in the schema.',
765
+ },
766
+
767
+ {
768
+ name: 'currentVehicleBrand',
769
+ renders: 'form-select (searchable)',
770
+ minimalNode: `{ "name": "currentVehicleBrand", "localization": { "en": { "label": "Current brand", "placeholder": "Choose a brand" } } }`,
771
+ needs: ['structure.data.currentVehiclesApi'],
772
+ notes:
773
+ 'The one field mapper that reads its endpoint from `structure.data` rather than from the node. Expects `{ ID, Name, Models: [{ ID, Name }] }`, appends a locale-provided `Other` option, and keeps each brand in `meta` so `currentVehicleModel` can read `Models` without a second request. Required in the schema when `ownVehicle === "yes"` — `requiredContext` does not reach it. Note the API filter tests lowercase `vehicle.name` while the mapping reads `vehicle.Name`, so an `Other` row coming from the endpoint is not actually filtered out and you get two.',
774
+ },
775
+
776
+ {
777
+ name: 'currentVehicleModel',
778
+ renders: 'form-select (searchable)',
779
+ minimalNode: `{ "name": "currentVehicleModel", "localization": { "en": { "label": "Current model", "placeholder": "Choose a model" } } }`,
780
+ needs: ['a currentVehicleBrand field in the same form'],
781
+ notes:
782
+ 'Reads `Models` off the selected brand `meta`; no request of its own. Disabled until a brand is chosen and again when the brand is `Other`. Required when `ownVehicle === "yes"` and the brand is not `Other`.',
783
+ },
784
+
785
+ {
786
+ name: 'conditionalCurrentVehicleBrand',
787
+ renders: 'form-select (searchable)',
788
+ minimalNode: `{ "name": "conditionalCurrentVehicleBrand", "brandApi": "https://example.invalid/api/vehicle-brands", "useNamedValue": true, "localization": { "en": { "label": "Current brand" } } }`,
789
+ needs: ['brandApi', 'an ownVehicle field in the same form'],
790
+ notes:
791
+ 'Same as `currentVehicleBrand` but takes its endpoint from the node (`brandApi`) and hard-gates on `ownVehicle === "yes"` for both enabled state and required state. `useNamedValue` submits the brand name instead of its id. Carries the same `name`/`Name` filter quirk.',
792
+ },
793
+
794
+ {
795
+ name: 'conditionalCurrentVehicleModel',
796
+ renders: 'form-select (searchable)',
797
+ minimalNode: `{ "name": "conditionalCurrentVehicleModel", "useNamedValue": true, "localization": { "en": { "label": "Current model" } } }`,
798
+ needs: ['a conditionalCurrentVehicleBrand field in the same form'],
799
+ notes: 'The gated twin of `currentVehicleModel`, reading `conditionalCurrentVehicleBrandList` from the form context.',
800
+ },
801
+
802
+ {
803
+ name: 'generalTicketType',
804
+ renders: 'form-select (clearable) — general-inquiry-form ONLY',
805
+ minimalNode: `{
806
+ "name": "generalTicketType",
807
+ "options": [
808
+ { "value": "support", "en": "Support", "ar": "دعم" },
809
+ { "value": "sales", "en": "Sales", "ar": "مبيعات" }
810
+ ],
811
+ "localization": { "en": { "label": "What is this about?", "placeholder": "Choose a topic", "require": "Please choose a topic." } }
812
+ }`,
813
+ needs: ['options'],
814
+ notes:
815
+ "The one field `general-inquiry-form` adds to the shared table, in both the mapper and the schema. Options are inline on the node — no request — with the label read from a per-language key on each option object (`option[language]`), not from a `label` field. Because it is in that component's schema, `requiredContext.generalTicketType` works. On the other four components the name is unknown and the node renders nothing.",
816
+ },
817
+ ];
818
+
819
+ // ---------------------------------------------------------------------------
820
+ // Steps
821
+ // ---------------------------------------------------------------------------
822
+
823
+ export const stepsReference = {
824
+ summary:
825
+ 'Adding `structure.steps` turns one render into n+1. `form-structure` runs `renderStructure` once with `currentStep === -1` — the chrome pass, drawn outside the sliding panels — and then once per step with `currentStep === 1 … n`, each into its own transform-animated panel. `step` on a node decides which passes it survives. Validation follows the same partition: `submitForm` omits every schema field that `hasItemInStructure` cannot find in the CURRENT step, so each step validates only its own fields; values accumulate in `stepFormValues`, and the POST happens only when `steps.length === currentStep`.',
826
+ example: `{
827
+ "data": { "requestUrl": "https://example.invalid/api/tickets" },
828
+ "requiredContext": { "name": true, "email": true, "vehicle": true },
829
+ "steps": [
830
+ {
831
+ "en": { "title": "Details", "stepCell": "1", "stepTitle": "Your details", "submitButton": "Continue" },
832
+ "ar": { "title": "البيانات", "stepCell": "١", "stepTitle": "بياناتك", "submitButton": "متابعة" }
833
+ },
834
+ {
835
+ "en": { "title": "Vehicle", "stepCell": "2", "stepTitle": "Your vehicle", "submitButton": "Book", "back": "Back" },
836
+ "ar": { "title": "المركبة", "stepCell": "٢", "stepTitle": "مركبتك", "submitButton": "احجز", "back": "رجوع" }
837
+ }
838
+ ],
839
+ "tag": "div",
840
+ "id": "container",
841
+ "children": [
842
+ { "name": "formStepper", "step": -1 },
843
+ {
844
+ "tag": "div",
845
+ "id": "step-one",
846
+ "step": 1,
847
+ "children": [
848
+ { "name": "name", "localization": { "en": { "label": "Full name" } } },
849
+ { "name": "email", "localization": { "en": { "label": "Email", "placeholder": "you@example.invalid" } } },
850
+ { "name": "stepperSubmit", "step": 1 }
851
+ ]
852
+ },
853
+ {
854
+ "tag": "div",
855
+ "id": "step-two",
856
+ "step": 2,
857
+ "children": [
858
+ { "name": "vehicle", "vehicleApi": "https://example.invalid/api/models", "localization": { "en": { "label": "Model" } } },
859
+ { "tag": "div", "id": "step-two-actions", "children": [{ "name": "back", "step": 2 }, { "name": "stepperSubmit", "step": 2 }] }
860
+ ]
861
+ }
862
+ ]
863
+ }`,
864
+ keys: [
865
+ {
866
+ key: 'title',
867
+ summary: 'Declared as the one REQUIRED key on the `Step` type.',
868
+ note: 'TRAP 9. Nothing reads it. No component, no helper — `getStepLabels` returns it in the object and every consumer takes `stepCell`, `stepTitle`, `submitButton` or `back` instead. TypeScript will make you write it; the rendered form will not show it. Treat it as a comment for whoever maintains the JSON.',
869
+ },
870
+ {
871
+ key: 'stepCell',
872
+ summary: 'The text inside the rail circle. Usually the step number — as a string, so it can be localised digits.',
873
+ note: 'Read by `form-stepper` from `structure.steps` directly, so the rail shows every step regardless of which pass it renders in.',
874
+ },
875
+ {
876
+ key: 'stepTitle',
877
+ summary: 'The caption under the rail circle.',
878
+ note: 'Also `form-stepper` only. Absent gives you a numbered circle with no caption.',
879
+ },
880
+ {
881
+ key: 'submitButton',
882
+ summary: 'Label for the `stepperSubmit` button on this step — "Continue" on the way through, "Book" at the end.',
883
+ note: 'Sits between the node localisation and the form-wide `submit` key in the fallback chain. The button reads it via its OWN `step`, not the current step, so give the node a `step`.',
884
+ },
885
+ {
886
+ key: 'back',
887
+ summary: 'Label for the `back` button on this step.',
888
+ note: 'Read from the CURRENT step, and there is no node-level override. Leave it out of step 1 (there is nothing to go back to) and put it on every later step.',
889
+ },
890
+ ],
891
+ traps: [
892
+ 'TRAP 4 — the one that actually bites. A node with NO `step` under a stepped root renders in the chrome pass AND in every step pass: n+1 copies. That means n+1 elements with the same `name`, n+1 identical ids, and n+1 `form.subscribe()` calls under one key. `getValues` keeps whichever copy the DOM walk reaches last, and a single `unsubscribe` removes all of them. Every node under a stepped root wants a `step`.',
893
+ '`step: -1` is the chrome pass and is where the stepper rail belongs. `step: 0` is falsy, so the guard treats it as "no step" during the step passes but still excludes it from the chrome pass — almost certainly not what you meant.',
894
+ 'Leaving `step` on a node after deleting `structure.steps` makes the node disappear: the single pass runs with `currentStep === -2`, which no positive step ever equals.',
895
+ '`step` is not stripped before the props are forwarded, so a container node ships a literal `step="1"` attribute into the DOM.',
896
+ "A field in another step is excluded from THIS step's validation by the same `hasItemInStructure` mechanism that `isHidden` uses — that is the feature, but it means a mis-stepped required field is never enforced anywhere.",
897
+ 'TRAP 10 — `stepChangeCallback` is declared as a `@Prop` on all five form components and typed on `FormHookInterface`, and is never invoked anywhere in the codebase. `updateStep` force-updates the back buttons and re-renders; it calls nothing. Do not wire analytics to it. To observe step changes today you have to poll `(await el.getForm()).formStructure.currentStep`.',
898
+ ],
899
+ };
900
+
901
+ // ---------------------------------------------------------------------------
902
+ // Required
903
+ // ---------------------------------------------------------------------------
904
+
905
+ export const requiredMechanics = {
906
+ knobs: [
907
+ {
908
+ id: 'requiredContext',
909
+ title: 'requiredContext — the one structure-level switch',
910
+ detail:
911
+ 'Every entry becomes a yup context variable with `Required` appended (`{"email": true}` → `$emailRequired`), and each base-schema field branches on its own variable through `.when("$<field>Required", { is: true, then: required, otherwise: optional })`. Read once, in the `FormHook` constructor. It works for exactly the fields that are IN the base schema: name, lastName, message, email, vin, vehicle, vacancyId, companyBranchId, cityId, date, year, time, ownVehicle, gender — plus generalTicketType on general-inquiry-form.',
912
+ example: `{ "requiredContext": { "name": true, "email": true, "companyBranchId": true, "date": true, "time": true } }`,
913
+ appliesTo: 'base-schema field names only',
914
+ },
915
+
916
+ {
917
+ id: 'required-prop',
918
+ title: 'required — a component prop, not a structure concept',
919
+ detail:
920
+ 'Only `form-file` and `form-phone-number` read it. On a file field it adds the "at least one file" test. Written on `email` or `name` it does nothing at all — those fields listen to `requiredContext`.',
921
+ example: `{ "name": "file", "required": true, "maxSize": 5, "signUrl": "https://example.invalid/api/sign" }`,
922
+ appliesTo: 'file, fileUploader, phone',
923
+ },
924
+
925
+ {
926
+ id: 'cross-field',
927
+ title: 'Value-driven requirements, hard-coded in the schema',
928
+ detail:
929
+ '`currentVehicleBrand` and `conditionalCurrentVehicleBrand` are required whenever the `ownVehicle` value is the literal string `yes`. `currentVehicleModel` and `conditionalCurrentVehicleModel` are required when `ownVehicle === "yes"` AND the chosen brand is not `Other`. These use `.when("ownVehicle", …)` on the VALUE, not on the context — so `requiredContext` cannot turn them on or off.',
930
+ example: `{
931
+ "tag": "div",
932
+ "children": [
933
+ { "name": "ownVehicle", "localization": { "en": { "label": "Do you own a vehicle?", "yes": "Yes", "no": "No" } } },
934
+ { "name": "currentVehicleBrand" },
935
+ { "name": "currentVehicleModel" }
936
+ ]
937
+ }`,
938
+ appliesTo: 'currentVehicleBrand, currentVehicleModel, conditionalCurrentVehicleBrand, conditionalCurrentVehicleModel',
939
+ },
940
+
941
+ {
942
+ id: 'isHidden-escape',
943
+ title: 'isHidden — the de-facto "do not validate this" switch',
944
+ detail:
945
+ '`submitForm` builds `excludedFields` from every schema field that `hasItemInStructure` cannot find, and `hasItemInStructure` refuses to match a node carrying `isHidden`. The field is then `omit`ed from the schema for that submit. The VALUE still goes out, because yup keeps unknown keys.',
946
+ example: `{ "name": "companyBranchId", "isHidden": true, "staticValue": { "value": "12", "label": "Central branch" }, "branchApi": "https://example.invalid/api/branches" }`,
947
+ appliesTo: 'any field — but see trap 3: only select-backed fields are actually hidden',
948
+ },
949
+
950
+ {
951
+ id: 'step-membership',
952
+ title: 'Step membership',
953
+ detail:
954
+ "The same `hasItemInStructure` walk bails out on `!!step && step != currentStep`, so a field belonging to another step is excluded from this step's validation. That is how per-step validation works — and how a mis-stepped field escapes validation entirely.",
955
+ example: `{ "tag": "div", "step": 2, "children": [{ "name": "vehicle" }] }`,
956
+ appliesTo: 'every field in a stepped form',
957
+ },
958
+
959
+ {
960
+ id: 'star-vs-enforcement',
961
+ title: 'The star and the enforcement are computed separately',
962
+ detail:
963
+ 'The asterisk comes from `FormHook.getInputState`, which probes the resolved schema with `validateSyncAt(name, { [name]: undefined })` and sets `isRequired = true` if ANYTHING throws. Enforcement comes from the omit-and-validate pass at submit. A field that is not in the schema at all makes `validateSyncAt` throw `The schema does not contain the path: <name>` — a permanent star that nothing enforces. The result is also CACHED on first call, so it never re-computes.',
964
+ example: `{ "name": "secondaryEmail", "type": "email", "localization": { "en": { "label": "Second email" } } }`,
965
+ appliesTo: 'bookingSlot, bookingDate, and any field name invented via `type`',
966
+ },
967
+ ],
968
+
969
+ exceptions: [
970
+ {
971
+ field: 'phone',
972
+ behaviour:
973
+ 'TRAP 5. Effectively always required, and there is no structure key that changes it. `phone` is not in the base schema, so `requiredContext.phone` is inert. Its schema comes from the component: a `require` test that passes when neither `required` nor `externalRequired` is set, followed by a `format` test that calls `value.replace(…)` unconditionally. `getInputState` probes with `undefined`, the format test throws a TypeError, the bare `catch` sets `isRequired = true`, and that value is cached AND fed straight back into the component as `externalRequired` — which is what makes the require test start enforcing. It is a loop that closes on the first render.',
974
+ escape:
975
+ 'None that leaves the field usable. `{ "name": "phone", "required": false }` and `{ "requiredContext": { "phone": false } }` both do nothing. The nearest thing is `{ "name": "phone", "isHidden": true }`, which drops it from the submit-time schema — but on a phone field `isHidden` does not hide anything, so you get a visible, starred, live-erroring field that merely stops blocking submit. If a form genuinely needs an optional phone number, leave the node out.',
976
+ },
977
+
978
+ {
979
+ field: 'bookingSlot / bookingDate',
980
+ behaviour:
981
+ 'TRAP 6. Always starred, never enforced. Both pickers implement `FormElement` but define no `validate()` and no `partialValidation()`, so `form.subscribe` shapes nothing into the schema and the name is simply absent from it. `getInputState` then throws on the missing path and returns `isRequired: true` forever, while `submitForm` has no field to check. An empty booking submits cleanly.',
982
+ escape:
983
+ 'There is no structure key for either half. `{ "name": "bookingSlot", "isRequired": false }` is a real prop on the picker, but the render reads `state?.isRequired || this.isRequired` and `state.isRequired` is already `true`, so it changes nothing. Enforcing a booking has to happen server-side or in a `middleware`/`successCallback` on the host page.',
984
+ },
985
+
986
+ {
987
+ field: 'file',
988
+ behaviour:
989
+ 'Honest, and the only field whose required state is genuinely node-driven. `{"required": true}` adds the count test and the star follows; without it the schema is `.optional()` and there is no star.',
990
+ escape: `{ "name": "file", "required": false, "maxSize": 5, "signUrl": "https://example.invalid/api/sign" }`,
991
+ },
992
+
993
+ {
994
+ field: 'gender',
995
+ behaviour:
996
+ 'TRAP 8. Present in the base schema as a `number()` with a full `$genderRequired` branch — and absent from the mapper table, on every one of the five components. `elementMapper["gender"]` is `undefined`, so `renderStructure` falls through and returns `false`: no field, no error, no warning. `requiredContext.gender` is therefore unreachable, because `hasItemInStructure` also cannot find it and `submitForm` omits it.',
997
+ escape:
998
+ 'Reuse a widget under that name: `{ "name": "gender", "type": "ownVehicle", "localization": { "en": { "label": "Gender", "yes": "Female", "no": "Male" } } }` renders a two-option select — but it submits the strings `yes`/`no` into a `number()` field, so the schema will reject it. In practice, do not use `gender`.',
999
+ },
1000
+
1001
+ {
1002
+ field: 'any name invented with `type`',
1003
+ behaviour:
1004
+ 'The value is collected and sent — `getValues` reads the DOM, and yup preserves unknown keys through `validate` — but the name is in no schema, so nothing validates it and `getInputState` throws on the missing path, leaving a permanent asterisk beside a field that can be submitted empty.',
1005
+ escape:
1006
+ 'Accept the star, and validate on the server — the value does arrive:\n' +
1007
+ '{ "name": "secondaryEmail", "type": "email", "localization": { "en": { "label": "Second email", "placeholder": "you@example.invalid" } } }\n' +
1008
+ 'posts as { "secondaryEmail": "someone@example.invalid" }. Adding the name to the schema would mean editing `src/components/forms/defaults/validation.ts` and shipping a new package version; there is no host-side hook. Same story for a new field TYPE — see trap 7 under `nodeProps` → `type`.',
1009
+ },
1010
+
1011
+ {
1012
+ field: 'generalTicketType',
1013
+ behaviour:
1014
+ 'Fully wired — mapper and schema — but only on `general-inquiry-form`. On `general-form`, `service-booking-form`, `ssc-lookup-form` and `test-drive-form` the name is unknown to both tables, so the node renders nothing and `requiredContext.generalTicketType` is inert.',
1015
+ escape: `{ "component": "general-inquiry-form", "requiredContext": { "generalTicketType": true } }`,
1016
+ },
1017
+ ],
1018
+ };
1019
+
1020
+ // ---------------------------------------------------------------------------
1021
+ // Marketing / UTM
1022
+ // ---------------------------------------------------------------------------
1023
+
1024
+ export const utm = {
1025
+ keys: ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid'],
1026
+
1027
+ readFrom: 'window.location.search on the page hosting the form, read at SUBMIT time — not at load. Only parameters actually present are collected; empty values are skipped.',
1028
+
1029
+ storedWhere:
1030
+ 'Nowhere. There is no cookie, no localStorage, no sessionStorage, no hidden field — `getMarketingValues` builds a fresh object from the live URL every time submit runs. Attribution therefore survives only as long as the query string does: if the visitor lands on a campaign URL and then navigates to a clean /contact page before filling the form, the parameters are gone. If you need them to survive navigation, the host page has to carry them, or feed them back through the `extraPayload` prop.',
1031
+
1032
+ sentAs:
1033
+ 'Merged into the JSON body for every method except GET and HEAD, where they are merged into the HEADERS instead (`["get", "head"].includes(method.toLowerCase())`). The merge happens after `extraPayload`, so a marketing key wins over a same-named key you set yourself.',
1034
+
1035
+ offSwitches: [
1036
+ {
1037
+ id: 'data.disableUTMLog',
1038
+ detail: 'A truthy value in the structure returns `{}` before the URL is even read. The permanent, per-form switch.',
1039
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets", "disableUTMLog": true } }`,
1040
+ },
1041
+ {
1042
+ id: 'disable_utm_log=true',
1043
+ detail:
1044
+ 'A URL query parameter on the hosting page, checked as an exact string match against `"true"`. Per-visit, and it does not need a structure change — handy for testing.',
1045
+ example: `{ "url": "https://example.invalid/contact?utm_source=newsletter&disable_utm_log=true", "collected": {} }`,
1046
+ },
1047
+ ],
1048
+
1049
+ example: `{
1050
+ "url": "https://example.invalid/contact?utm_source=newsletter&utm_campaign=spring&gclid=abc123&ref=ignored",
1051
+ "collectedValues": {
1052
+ "utm_source": "newsletter",
1053
+ "utm_campaign": "spring",
1054
+ "gclid": "abc123"
1055
+ },
1056
+ "postBody": {
1057
+ "name": "Ada Lovelace",
1058
+ "email": "ada@example.invalid",
1059
+ "message": "Please call me back.",
1060
+ "utm_source": "newsletter",
1061
+ "utm_campaign": "spring",
1062
+ "gclid": "abc123"
1063
+ }
1064
+ }`,
1065
+ };
1066
+
1067
+ // ---------------------------------------------------------------------------
1068
+ // The wire
1069
+ // ---------------------------------------------------------------------------
1070
+
1071
+ export const wireFormat = {
1072
+ headers: [
1073
+ {
1074
+ name: 'Content-Type',
1075
+ value: 'application/json',
1076
+ when: 'Always — including on a GET, where no body is sent.',
1077
+ example: `{ "Content-Type": "application/json" }`,
1078
+ },
1079
+ {
1080
+ name: 'Brand',
1081
+ value: 'structure.data.brandId',
1082
+ when: 'Always. The key is written unconditionally, so with no `brandId` the header goes out as the literal string "undefined" rather than being omitted.',
1083
+ example: `{ "Brand": "demo-brand" }`,
1084
+ },
1085
+ {
1086
+ name: 'Accept-Language',
1087
+ value: 'the form\'s current language, falling back to "en"',
1088
+ when: 'Always. Single value, never a weighted list.',
1089
+ example: `{ "Accept-Language": "ar" }`,
1090
+ },
1091
+ {
1092
+ name: 'Recaptcha-Token',
1093
+ value: 'grecaptcha.execute(data.recaptchaKey, { action: "submit" })',
1094
+ when: 'Browser mode only, and only when `data.recaptchaKey` is set. Omit the key and this header never appears — which is what every example on this page does.',
1095
+ example: `{ "data": { "requestUrl": "https://example.invalid/api/tickets" } }`,
1096
+ },
1097
+ {
1098
+ name: 'Authorization',
1099
+ value: 'the value returned by the getMobileToken prop',
1100
+ when: 'Mobile mode, when that token starts (case-insensitively) with `bearer`. The endpoint then comes from `data.requestAppUrl`.',
1101
+ example: `{ "data": { "isMobileForm": true, "requestAppUrl": "https://example.invalid/api/app/tickets" } }`,
1102
+ },
1103
+ {
1104
+ name: 'verification-token',
1105
+ value: 'the value returned by the getMobileToken prop',
1106
+ when: 'Mobile mode, when the token does NOT start with `bearer`. The endpoint then comes from `data.requestAppCheckUrl`.',
1107
+ example: `{ "data": { "isMobileForm": true, "requestAppCheckUrl": "https://example.invalid/api/appcheck/tickets" } }`,
1108
+ },
1109
+ {
1110
+ name: '<your own>',
1111
+ value: 'extraHeader prop, then structure.data.extraHeader',
1112
+ when: 'Whenever set. Note the order: the STRUCTURE is applied last and wins. The payload merge is the other way round, where the PROP wins.',
1113
+ example: `{ "data": { "extraHeader": { "X-Source": "docs-site", "X-Tenant": "demo" } } }`,
1114
+ },
1115
+ {
1116
+ name: '<marketing keys>',
1117
+ value: 'utm_source, utm_medium, utm_campaign, utm_term, utm_content, gclid, fbclid',
1118
+ when: 'Only when `data.requestMethod` is `get` or `head`. On every other method they go into the body instead.',
1119
+ example: `{ "data": { "requestMethod": "GET", "requestUrl": "https://example.invalid/api/tickets" } }`,
1120
+ },
1121
+ ],
1122
+
1123
+ successPath:
1124
+ '`response.ok` → the body is parsed with `response.json().catch(() => ({}))`, so a non-JSON 200 becomes `{}` rather than an error. The result is handed to `setSuccessCallback` → `formSuccessHandler`, which calls your `successCallback(data, message)` if you supplied one. THE RETURN VALUE OF THAT CALLBACK DECIDES THE DIALOG: `openDefaultDialog = !!(await successCallback(...))`, so a callback that returns nothing SUPPRESSES the built-in success dialog. Return `true` to keep it. With no callback at all the dialog always opens. Then the page scrolls the form into view (unless `disableScrollToTop`), and 100 ms later the form resets and re-renders.',
1125
+
1126
+ errorPath:
1127
+ 'Not `response.ok` → if the response `content-type` includes `application/json`, the message is read from `parsedResponse.message.body ?? parsedResponse.Message.Body ?? ""`; otherwise the raw `response.text()` is used. That string is thrown as an `Error` and caught, then `formErrorHandler` resolves the display message as `error.message || error.Message.Body || error.message.body || sharedFormLocales.errors.wildCard`. So an error body in any other shape degrades to the generic wildcard string. `errorCallback` follows the same suppression rule as the success one: return a truthy value to keep the built-in error dialog. `isLoading` is cleared in a `finally`, so a thrown error never leaves the button spinning.',
1128
+
1129
+ example: `{
1130
+ "request": {
1131
+ "url": "https://example.invalid/api/tickets",
1132
+ "method": "POST",
1133
+ "headers": {
1134
+ "Content-Type": "application/json",
1135
+ "Brand": "demo-brand",
1136
+ "Accept-Language": "en",
1137
+ "X-Source": "docs-site"
1138
+ },
1139
+ "body": {
1140
+ "name": "Ada Lovelace",
1141
+ "email": "ada@example.invalid",
1142
+ "phone": "+964 750 000 0000",
1143
+ "date": "2026-09-14",
1144
+ "time": "09:30",
1145
+ "preferredDateTime": "2026-09-14T09:30:00+03:00",
1146
+ "additionalData": { "customerNotes": "Please call me back." },
1147
+ "utm_source": "newsletter"
1148
+ }
1149
+ },
1150
+ "successResponse": { "status": 200, "body": { "id": "TCK-1024" } },
1151
+ "errorResponse": { "status": 400, "body": { "message": { "body": "That branch is closed on the selected day." } } }
1152
+ }`,
1153
+ };