@sankhyatronics/sankhya-cms 0.1.18 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/custom-elements.json +11634 -3153
  2. package/dist/attention-panel/index.js +2 -2
  3. package/dist/bar-chart/index.js +2 -2
  4. package/dist/bento-grid/index.d.ts +33 -1
  5. package/dist/bento-grid/index.js +4 -3
  6. package/dist/booking/index.d.ts +309 -0
  7. package/dist/booking/index.js +20 -0
  8. package/dist/booking/index.js.map +1 -0
  9. package/dist/card/index.d.ts +3 -4
  10. package/dist/card/index.js +2 -2
  11. package/dist/carousel/index.d.ts +20 -6
  12. package/dist/carousel/index.js +3 -3
  13. package/dist/chip/index.js +2 -2
  14. package/dist/{chunk-JQY2G4QK.js → chunk-2GTR37PU.js} +2 -2
  15. package/dist/chunk-2GTR37PU.js.map +1 -0
  16. package/dist/{chunk-FLY7TNRT.js → chunk-2HMOX253.js} +128 -25
  17. package/dist/chunk-2HMOX253.js.map +1 -0
  18. package/dist/{chunk-DH53HZPB.js → chunk-37KFBKPD.js} +32 -4
  19. package/dist/chunk-37KFBKPD.js.map +1 -0
  20. package/dist/{chunk-ALI26WOB.js → chunk-4XPKKYVZ.js} +54 -15
  21. package/dist/chunk-4XPKKYVZ.js.map +1 -0
  22. package/dist/chunk-535QAZ6A.js +236 -0
  23. package/dist/chunk-535QAZ6A.js.map +1 -0
  24. package/dist/{chunk-AKJQ345O.js → chunk-5AWGZQWU.js} +2 -2
  25. package/dist/chunk-6OZKZG5J.js +300 -0
  26. package/dist/chunk-6OZKZG5J.js.map +1 -0
  27. package/dist/chunk-AYNT7G3J.js +805 -0
  28. package/dist/chunk-AYNT7G3J.js.map +1 -0
  29. package/dist/chunk-BEWX3E6P.js +32 -0
  30. package/dist/chunk-BEWX3E6P.js.map +1 -0
  31. package/dist/chunk-CZUGPKXV.js +275 -0
  32. package/dist/chunk-CZUGPKXV.js.map +1 -0
  33. package/dist/{chunk-LEXZKGQX.js → chunk-DJFWZOZM.js} +107 -11
  34. package/dist/chunk-DJFWZOZM.js.map +1 -0
  35. package/dist/chunk-DPANTC3O.js +1714 -0
  36. package/dist/chunk-DPANTC3O.js.map +1 -0
  37. package/dist/chunk-EKZFFY45.js +426 -0
  38. package/dist/chunk-EKZFFY45.js.map +1 -0
  39. package/dist/chunk-ER7MCPDV.js +7 -0
  40. package/dist/chunk-ER7MCPDV.js.map +1 -0
  41. package/dist/chunk-EW2RK5Y6.js +383 -0
  42. package/dist/chunk-EW2RK5Y6.js.map +1 -0
  43. package/dist/{chunk-BLQV2YMO.js → chunk-EZOKT7NJ.js} +2 -2
  44. package/dist/chunk-FF2LWEF3.js +386 -0
  45. package/dist/chunk-FF2LWEF3.js.map +1 -0
  46. package/dist/chunk-FOUK3NHR.js +7 -0
  47. package/dist/chunk-FOUK3NHR.js.map +1 -0
  48. package/dist/chunk-GB4NGCZI.js +181 -0
  49. package/dist/chunk-GB4NGCZI.js.map +1 -0
  50. package/dist/{chunk-7NLZND6Q.js → chunk-GOFSMD6W.js} +35 -11
  51. package/dist/chunk-GOFSMD6W.js.map +1 -0
  52. package/dist/{chunk-6D6ROU7X.js → chunk-IGMVF3KK.js} +2 -2
  53. package/dist/{chunk-INMSEYT6.js → chunk-IVPVYVWG.js} +2 -2
  54. package/dist/chunk-JURCF4OD.js +381 -0
  55. package/dist/chunk-JURCF4OD.js.map +1 -0
  56. package/dist/{chunk-FZ36KL73.js → chunk-KZ7PMFWS.js} +11 -1
  57. package/dist/chunk-KZ7PMFWS.js.map +1 -0
  58. package/dist/{chunk-EMCQBT3S.js → chunk-LO6TZBGD.js} +129 -35
  59. package/dist/chunk-LO6TZBGD.js.map +1 -0
  60. package/dist/chunk-MFRGAT6D.js +860 -0
  61. package/dist/chunk-MFRGAT6D.js.map +1 -0
  62. package/dist/chunk-MIBVQXRU.js +1098 -0
  63. package/dist/chunk-MIBVQXRU.js.map +1 -0
  64. package/dist/{chunk-35TX3AOA.js → chunk-NABSRUXG.js} +2 -2
  65. package/dist/{chunk-R5G6KFTJ.js → chunk-NXKDZWS7.js} +44 -6
  66. package/dist/chunk-NXKDZWS7.js.map +1 -0
  67. package/dist/{chunk-KMU6ABQF.js → chunk-NXUS2CH7.js} +14 -1
  68. package/dist/chunk-NXUS2CH7.js.map +1 -0
  69. package/dist/{chunk-GJO3RGOB.js → chunk-OCPGOLVF.js} +56 -11
  70. package/dist/chunk-OCPGOLVF.js.map +1 -0
  71. package/dist/chunk-OEOE6IFY.js +628 -0
  72. package/dist/chunk-OEOE6IFY.js.map +1 -0
  73. package/dist/{chunk-SMLR736E.js → chunk-PV6OCXXN.js} +2 -2
  74. package/dist/chunk-PV6OCXXN.js.map +1 -0
  75. package/dist/chunk-Q66CJO3P.js +534 -0
  76. package/dist/chunk-Q66CJO3P.js.map +1 -0
  77. package/dist/{chunk-MG2VTZA4.js → chunk-QS3ANOWN.js} +2 -2
  78. package/dist/{chunk-ZN4LIKVG.js → chunk-QSJ543V2.js} +2 -2
  79. package/dist/chunk-QSJ543V2.js.map +1 -0
  80. package/dist/{chunk-FML6AG2V.js → chunk-RANC3OI6.js} +2 -2
  81. package/dist/chunk-T4BIN37Q.js +32 -0
  82. package/dist/chunk-T4BIN37Q.js.map +1 -0
  83. package/dist/{chunk-YVTFQHVY.js → chunk-TEAZZPDQ.js} +2 -2
  84. package/dist/chunk-TJK55CJ2.js +49 -0
  85. package/dist/chunk-TJK55CJ2.js.map +1 -0
  86. package/dist/chunk-UKIVLF7S.js +32 -0
  87. package/dist/chunk-UKIVLF7S.js.map +1 -0
  88. package/dist/chunk-V4R7PYP5.js +905 -0
  89. package/dist/chunk-V4R7PYP5.js.map +1 -0
  90. package/dist/{chunk-AG2COC64.js → chunk-VVYQ2DDS.js} +2 -2
  91. package/dist/chunk-WAMOAAMN.js +16 -0
  92. package/dist/chunk-WAMOAAMN.js.map +1 -0
  93. package/dist/chunk-WAYHHL4J.js +1234 -0
  94. package/dist/chunk-WAYHHL4J.js.map +1 -0
  95. package/dist/{chunk-YQ42EBK5.js → chunk-WH6HE6SY.js} +10 -4
  96. package/dist/chunk-WH6HE6SY.js.map +1 -0
  97. package/dist/chunk-XP6DI3VY.js +180 -0
  98. package/dist/chunk-XP6DI3VY.js.map +1 -0
  99. package/dist/{chunk-5EBWMPJS.js → chunk-XS6LNTSH.js} +76 -14
  100. package/dist/chunk-XS6LNTSH.js.map +1 -0
  101. package/dist/{chunk-WT4OATGP.js → chunk-ZKFCDVB4.js} +16 -7
  102. package/dist/chunk-ZKFCDVB4.js.map +1 -0
  103. package/dist/cms-theme.css +29 -0
  104. package/dist/contact-form/index.d.ts +220 -0
  105. package/dist/contact-form/index.js +16 -0
  106. package/dist/contact-form/index.js.map +1 -0
  107. package/dist/content-block/index.d.ts +39 -4
  108. package/dist/content-block/index.js +8 -4
  109. package/dist/cookie-consent/index.d.ts +60 -12
  110. package/dist/cookie-consent/index.js +3 -2
  111. package/dist/date-tile/index.js +2 -2
  112. package/dist/dropdown/index.d.ts +29 -11
  113. package/dist/dropdown/index.js +4 -2
  114. package/dist/feature-split/index.d.ts +6 -1
  115. package/dist/feature-split/index.js +5 -4
  116. package/dist/features-section/index.d.ts +53 -9
  117. package/dist/features-section/index.js +6 -5
  118. package/dist/flow/index.d.ts +35 -0
  119. package/dist/flow/index.js +9 -0
  120. package/dist/flow/index.js.map +1 -0
  121. package/dist/footer/index.d.ts +13 -0
  122. package/dist/footer/index.js +2 -2
  123. package/dist/header/index.d.ts +135 -16
  124. package/dist/header/index.js +3 -2
  125. package/dist/hero/index.d.ts +92 -8
  126. package/dist/hero/index.js +6 -3
  127. package/dist/icon-button/index.d.ts +7 -7
  128. package/dist/icon-button/index.js +2 -2
  129. package/dist/index.d.ts +12 -3
  130. package/dist/index.js +121 -47
  131. package/dist/items-accordion/index.d.ts +9 -0
  132. package/dist/items-accordion/index.js +4 -3
  133. package/dist/kpi-card/index.js +2 -2
  134. package/dist/markdown/index.d.ts +19 -2
  135. package/dist/markdown/index.js +3 -2
  136. package/dist/mega-menu/index.d.ts +95 -7
  137. package/dist/mega-menu/index.js +7 -2
  138. package/dist/menu-grid/index.d.ts +2 -3
  139. package/dist/menu-grid/index.js +2 -2
  140. package/dist/menu-grid-item/index.d.ts +6 -8
  141. package/dist/menu-grid-item/index.js +2 -2
  142. package/dist/menu-item/index.d.ts +11 -7
  143. package/dist/menu-item/index.js +3 -2
  144. package/dist/plan-cards/index.d.ts +69 -0
  145. package/dist/plan-cards/index.js +13 -0
  146. package/dist/plan-cards/index.js.map +1 -0
  147. package/dist/product-nav/index.d.ts +127 -0
  148. package/dist/product-nav/index.js +11 -0
  149. package/dist/product-nav/index.js.map +1 -0
  150. package/dist/promo-banner/index.d.ts +42 -4
  151. package/dist/promo-banner/index.js +6 -3
  152. package/dist/quote-dialog/index.d.ts +174 -0
  153. package/dist/quote-dialog/index.js +22 -0
  154. package/dist/quote-dialog/index.js.map +1 -0
  155. package/dist/schema/index.d.ts +11 -0
  156. package/dist/schema/index.js +9 -0
  157. package/dist/schema/index.js.map +1 -1
  158. package/dist/screenshot-tour/index.d.ts +54 -0
  159. package/dist/screenshot-tour/index.js +9 -0
  160. package/dist/screenshot-tour/index.js.map +1 -0
  161. package/dist/section/index.d.ts +3 -0
  162. package/dist/section/index.js +3 -3
  163. package/dist/segmented-control/index.js +2 -2
  164. package/dist/select/index.js +2 -2
  165. package/dist/shared/styles.d.ts +7 -1
  166. package/dist/shared/styles.js +3 -1
  167. package/dist/stats/index.js +2 -2
  168. package/dist/steps/index.d.ts +45 -0
  169. package/dist/steps/index.js +11 -0
  170. package/dist/steps/index.js.map +1 -0
  171. package/dist/tag-strip/index.d.ts +33 -0
  172. package/dist/tag-strip/index.js +9 -0
  173. package/dist/tag-strip/index.js.map +1 -0
  174. package/dist/testimonials/index.d.ts +9 -1
  175. package/dist/testimonials/index.js +3 -3
  176. package/dist/tooltip/index.js +2 -2
  177. package/package.json +38 -2
  178. package/dist/chunk-5EBWMPJS.js.map +0 -1
  179. package/dist/chunk-76AVBQUA.js +0 -252
  180. package/dist/chunk-76AVBQUA.js.map +0 -1
  181. package/dist/chunk-7NLZND6Q.js.map +0 -1
  182. package/dist/chunk-ALI26WOB.js.map +0 -1
  183. package/dist/chunk-BMHMNXIA.js +0 -158
  184. package/dist/chunk-BMHMNXIA.js.map +0 -1
  185. package/dist/chunk-DH53HZPB.js.map +0 -1
  186. package/dist/chunk-EMCQBT3S.js.map +0 -1
  187. package/dist/chunk-FLY7TNRT.js.map +0 -1
  188. package/dist/chunk-FZ36KL73.js.map +0 -1
  189. package/dist/chunk-GJO3RGOB.js.map +0 -1
  190. package/dist/chunk-J6EO4R73.js +0 -633
  191. package/dist/chunk-J6EO4R73.js.map +0 -1
  192. package/dist/chunk-JKBWZLG4.js +0 -348
  193. package/dist/chunk-JKBWZLG4.js.map +0 -1
  194. package/dist/chunk-JQY2G4QK.js.map +0 -1
  195. package/dist/chunk-KMU6ABQF.js.map +0 -1
  196. package/dist/chunk-LEXZKGQX.js.map +0 -1
  197. package/dist/chunk-MSSH3HFK.js +0 -205
  198. package/dist/chunk-MSSH3HFK.js.map +0 -1
  199. package/dist/chunk-R5G6KFTJ.js.map +0 -1
  200. package/dist/chunk-SMLR736E.js.map +0 -1
  201. package/dist/chunk-VWS53WY4.js +0 -157
  202. package/dist/chunk-VWS53WY4.js.map +0 -1
  203. package/dist/chunk-WT4OATGP.js.map +0 -1
  204. package/dist/chunk-XWB4CKDU.js +0 -708
  205. package/dist/chunk-XWB4CKDU.js.map +0 -1
  206. package/dist/chunk-YQ42EBK5.js.map +0 -1
  207. package/dist/chunk-ZN4LIKVG.js.map +0 -1
  208. /package/dist/{chunk-AKJQ345O.js.map → chunk-5AWGZQWU.js.map} +0 -0
  209. /package/dist/{chunk-BLQV2YMO.js.map → chunk-EZOKT7NJ.js.map} +0 -0
  210. /package/dist/{chunk-6D6ROU7X.js.map → chunk-IGMVF3KK.js.map} +0 -0
  211. /package/dist/{chunk-INMSEYT6.js.map → chunk-IVPVYVWG.js.map} +0 -0
  212. /package/dist/{chunk-35TX3AOA.js.map → chunk-NABSRUXG.js.map} +0 -0
  213. /package/dist/{chunk-MG2VTZA4.js.map → chunk-QS3ANOWN.js.map} +0 -0
  214. /package/dist/{chunk-FML6AG2V.js.map → chunk-RANC3OI6.js.map} +0 -0
  215. /package/dist/{chunk-YVTFQHVY.js.map → chunk-TEAZZPDQ.js.map} +0 -0
  216. /package/dist/{chunk-AG2COC64.js.map → chunk-VVYQ2DDS.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/quote-dialog/QuoteDialog.ts","../src/shared/mailto.ts","../src/quote-dialog/quote-mailto.ts"],"sourcesContent":["import { LitElement, css, html, nothing, svg } from 'lit';\nimport { customElement, property, query, state } from 'lit/decorators.js';\n\nimport { setModalOpen } from '../shared/modal-state';\nimport { controlBaseStyles, fontFamilyStyles } from '../shared/styles';\nimport { QUOTE_OPEN_EVENT, type QuoteOpenEventDetail, type QuoteSubmitEventDetail } from './QuoteDialogEvents';\nimport { buildQuoteMailto, fillTemplate } from './quote-mailto';\n\nexport interface QuoteOption {\n id: string;\n label: string;\n hint?: string;\n /** Short name for the e-mail subject's `{option}` (e.g. \"Cloud\" for \"Cloud (SaaS)\"). Defaults to `label`. */\n subjectLabel?: string;\n}\n\ntype FieldName = 'name' | 'email' | 'company' | 'consent';\n\nconst CLOSE_ICON = svg`<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6 6l12 12M18 6 6 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"></path></svg>`;\n\nconst SUCCESS_ICON = svg`<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 12l5 5L20 7\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg>`;\n\n/**\n * A quote-request dialog: radio cards for the kind of request, then name, e-mail, company, a user\n * count, an optional message and a required consent box. It opens when a window `st-quote-open`\n * event names its `id` (`st-plan-cards` does this on a button click), with that option preselected.\n *\n * It is a native `<dialog>` opened with `showModal()`, so focus is trapped, Esc closes it and the page\n * behind is inert. Closing (Esc, the close button or a click on the backdrop) returns focus to the\n * element that opened it. While open, `<html>` carries `data-st-modal-open` (removed on every close and\n * on disconnect), so other fixed page UI such as `st-cookie-consent` steps aside.\n * Submitting checks the fields, opens a `mailto:` message to `mailto` with\n * every answer in the body, dispatches `quote-submit` and shows `success-title` / `success-text`\n * (`{option}` in the text becomes the chosen option's label). Without `mailto` nothing is opened and\n * the success state still shows. `subject` may use `{option}` (the option's `subjectLabel`, else its\n * label), `{company}` and `{name}`. `mailto-note` is shown under the success text (`{email}` becomes\n * the `mailto` address, as a link) with a button that copies the address, for visitors whose mail\n * app did not open. An empty `consent-label` leaves out the consent box.\n *\n * The element's `id` is the dialog name: set it on the JSON node (`\"id\": \"quote\"`), and point\n * `quoteDialog` on a `st-plan-cards` at the same value.\n *\n * @fires quote-submit - Bubbles and is composed. Detail `{ option, name, email, company, count, message, consent }`.\n * @cssprop --st-quote-dialog-backdrop - Colour of the backdrop behind the dialog (default a navy at 55%).\n * @csspart dialog - The native dialog.\n * @csspart form - The request form.\n * @csspart submit - The submit button.\n * @csspart close - The close button.\n */\n@customElement('st-quote-dialog')\nexport class QuoteDialog extends LitElement {\n static styles = css`\n ${controlBaseStyles}\n /* fontFamilyStyles is a bare declaration: it must sit inside a rule, or it swallows the next one. */\n :host {\n ${fontFamilyStyles}\n }\n\n :host {\n display: block;\n }\n\n dialog {\n box-sizing: border-box;\n width: calc(100% - 32px);\n max-width: 560px;\n max-height: calc(100vh - 32px);\n margin: auto;\n padding: 0;\n border: 0;\n border-radius: 24px;\n background: var(--st-color-surface);\n color: var(--st-color-text-strong);\n box-shadow: 0 20px 40px -12px #0000001a;\n overflow: auto;\n }\n\n dialog::backdrop {\n background: var(--st-quote-dialog-backdrop, rgba(1, 29, 65, 0.55));\n }\n\n .panel {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 20px;\n padding: 32px;\n }\n\n .header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n }\n\n .heading {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .title {\n margin: 0;\n font-size: 24px;\n font-weight: var(--st-font-weight-bold);\n line-height: 1.25;\n color: var(--st-cms-text-strong, var(--st-cms-navy));\n }\n\n .subtitle {\n margin: 0;\n font-size: 15px;\n line-height: 1.5;\n color: var(--st-cms-text, var(--st-cms-navy-2));\n }\n\n .close {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--st-radius-full);\n background: var(--st-cms-surface-soft);\n color: var(--st-cms-text-strong, var(--st-cms-navy));\n cursor: pointer;\n }\n\n .close:hover {\n background: var(--st-cms-primary-100);\n }\n\n .close svg {\n width: 20px;\n height: 20px;\n }\n\n .close:focus-visible,\n .submit:focus-visible,\n .done:focus-visible,\n .option input:focus-visible,\n .input:focus-visible,\n .consent input:focus-visible {\n outline: var(--st-focus-ring);\n outline-offset: 2px;\n }\n\n form {\n display: flex;\n flex-direction: column;\n gap: var(--st-space-16);\n }\n\n .options {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n .legend {\n padding: 0;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: var(--st-font-weight-semibold);\n color: var(--st-cms-text-strong, var(--st-cms-navy));\n }\n\n .option-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n }\n\n .option {\n flex: 1 1 150px;\n display: flex;\n align-items: flex-start;\n gap: 10px;\n box-sizing: border-box;\n min-height: 44px;\n padding: 12px 14px;\n border: 1px solid var(--st-cms-primary-200);\n border-radius: 10px;\n background: var(--st-color-surface);\n cursor: pointer;\n }\n\n .option.selected {\n border: 2px solid var(--st-cms-brand-navy);\n /* Keep the text where it is when the border thickens. */\n padding: 11px 13px;\n }\n\n .option input {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 3px 0 0;\n accent-color: var(--st-cms-brand-navy);\n }\n\n .option-body {\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n .option-label {\n font-size: 14px;\n font-weight: var(--st-font-weight-semibold);\n color: var(--st-cms-text-strong, var(--st-cms-navy));\n }\n\n .option-hint {\n font-size: 12px;\n color: var(--st-cms-text, var(--st-cms-navy-2));\n }\n\n .fields {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 12px;\n }\n\n .field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .label {\n font-size: 14px;\n font-weight: var(--st-font-weight-semibold);\n color: var(--st-cms-text-strong, var(--st-cms-navy));\n }\n\n .optional {\n font-weight: var(--st-font-weight-medium);\n color: var(--st-cms-text, var(--st-cms-navy-2));\n }\n\n .input {\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n padding: var(--st-space-8) var(--st-space-12);\n border: 1px solid var(--st-color-border-strong);\n border-radius: 8px;\n background: var(--st-color-surface);\n color: var(--st-cms-text-strong, var(--st-cms-navy));\n /* 16px stops iOS zooming into the field. */\n font: inherit;\n font-size: 16px;\n }\n\n textarea.input {\n min-height: 96px;\n resize: vertical;\n }\n\n .input[aria-invalid='true'] {\n border: 2px solid var(--st-color-danger);\n }\n\n .error {\n margin: 0;\n font-size: 13px;\n color: var(--st-color-danger-text);\n }\n\n /* The always-present polite live region: announces the error summary, never shown. */\n .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n white-space: nowrap;\n border: 0;\n }\n\n .consent {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--st-cms-text, var(--st-cms-navy-2));\n }\n\n .consent input {\n flex: none;\n width: 16px;\n height: 16px;\n margin: 2px 0 0;\n accent-color: var(--st-cms-brand-navy);\n }\n\n .submit {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n min-height: 48px;\n margin-top: var(--st-space-8);\n padding: 0 var(--st-space-24);\n border: 0;\n border-radius: var(--st-radius-full);\n background: var(--st-cms-brand-navy);\n color: var(--st-cms-on-dark);\n font: inherit;\n font-size: 16px;\n font-weight: var(--st-font-weight-semibold);\n cursor: pointer;\n }\n\n .success {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n padding: 16px 0 8px;\n text-align: center;\n }\n\n .success-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n border-radius: var(--st-radius-full);\n background: var(--st-cms-highlight);\n color: var(--st-cms-navy);\n }\n\n .success-icon svg {\n width: 28px;\n height: 28px;\n }\n\n .success-title {\n margin: 0;\n font-size: 20px;\n font-weight: var(--st-font-weight-semibold);\n color: var(--st-cms-text-strong, var(--st-cms-navy));\n }\n\n .success-text {\n max-width: 400px;\n margin: 0;\n font-size: 15px;\n line-height: 1.6;\n color: var(--st-cms-text, var(--st-cms-navy-2));\n }\n\n .mailto-note {\n max-width: 400px;\n margin: 0;\n font-size: 14px;\n line-height: 1.6;\n color: var(--st-cms-text, var(--st-cms-navy-2));\n }\n\n .mailto-note a {\n color: var(--st-cms-accent-text, var(--st-cms-brand-navy));\n font-weight: var(--st-font-weight-semibold);\n }\n\n .success-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 12px;\n }\n\n .done:focus-visible,\n .copy:focus-visible {\n outline: 2px solid var(--st-cms-brand-azure);\n outline-offset: 2px;\n }\n\n .done,\n .copy {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 44px;\n padding: 0 24px;\n border: 1px solid var(--st-cms-primary-200);\n border-radius: var(--st-radius-full);\n background: var(--st-color-surface);\n color: var(--st-cms-accent-text, var(--st-cms-brand-navy));\n font: inherit;\n font-weight: var(--st-font-weight-semibold);\n cursor: pointer;\n }\n\n @media (max-width: 600px) {\n .panel {\n padding: 24px;\n }\n }\n `;\n\n /** Heading of the dialog (an `<h2>`, named by the dialog). */\n @property({ type: String })\n title = '';\n\n @property({ type: String })\n subtitle = '';\n\n /** The radio cards for the kind of request. */\n @property({ attribute: false })\n options: QuoteOption[] = [];\n\n @property({ type: String, attribute: 'options-label' })\n optionsLabel = 'Choose an option';\n\n @property({ type: String, attribute: 'name-label' })\n nameLabel = 'Name';\n\n @property({ type: String, attribute: 'name-placeholder' })\n namePlaceholder = '';\n\n @property({ type: String, attribute: 'email-label' })\n emailLabel = 'Email';\n\n @property({ type: String, attribute: 'email-placeholder' })\n emailPlaceholder = '';\n\n @property({ type: String, attribute: 'company-label' })\n companyLabel = 'Company';\n\n @property({ type: String, attribute: 'company-placeholder' })\n companyPlaceholder = '';\n\n @property({ type: String, attribute: 'count-label' })\n countLabel = 'Number of users';\n\n @property({ type: String, attribute: 'count-placeholder' })\n countPlaceholder = '';\n\n /** Label of the optional message box. \"(optional)\" is appended. */\n @property({ type: String, attribute: 'message-label' })\n messageLabel = 'Message';\n\n @property({ type: String, attribute: 'message-placeholder' })\n messagePlaceholder = '';\n\n @property({ type: String, attribute: 'consent-label' })\n consentLabel = 'I agree to be contacted about this request.';\n\n @property({ type: String, attribute: 'submit-label' })\n submitLabel = 'Send request';\n\n @property({ type: String, attribute: 'success-title' })\n successTitle = 'Thank you';\n\n /** Shown after submitting. `{option}` is replaced with the chosen option's label. */\n @property({ type: String, attribute: 'success-text' })\n successText = '';\n\n @property({ type: String, attribute: 'close-label' })\n closeLabel = 'Close';\n\n /** Recipient of the request e-mail. Without it, submitting opens no e-mail. */\n @property({ type: String })\n mailto = '';\n\n /** Subject of the request e-mail. `{option}`, `{company}` and `{name}` are filled in. */\n @property({ type: String })\n subject = 'Quote request';\n\n /** Shown under the success text when `mailto` is set. `{email}` becomes the address as a link. Empty hides it. */\n @property({ type: String, attribute: 'mailto-note' })\n mailtoNote = '';\n\n @property({ type: String, attribute: 'copy-label' })\n copyLabel = 'Copy address';\n\n @property({ type: String, attribute: 'copied-label' })\n copiedLabel = 'Address copied';\n\n @state() private selected = '';\n @state() private copied = false;\n @state() private submitted = false;\n @state() private errors: Partial<Record<FieldName, string>> = {};\n @state() private submittedOption = '';\n\n @query('dialog') private dialogEl?: HTMLDialogElement;\n @query('form') private formEl?: HTMLFormElement;\n\n private trigger: HTMLElement | null = null;\n\n connectedCallback(): void {\n super.connectedCallback();\n window.addEventListener(QUOTE_OPEN_EVENT, this.onOpenRequest);\n }\n\n disconnectedCallback(): void {\n window.removeEventListener(QUOTE_OPEN_EVENT, this.onOpenRequest);\n setModalOpen(this, false);\n super.disconnectedCallback();\n }\n\n /** Opens for `st-quote-open` events aimed at this dialog's `id`. */\n private onOpenRequest = (event: Event): void => {\n const detail = (event as CustomEvent<QuoteOpenEventDetail>).detail;\n if (!detail || !this.id || detail.dialog !== this.id) return;\n this.trigger = detail.trigger ?? null;\n const known = this.options.some(option => option.id === detail.option);\n this.selected = known ? (detail.option as string) : (this.options[0]?.id ?? '');\n this.submitted = false;\n this.copied = false;\n this.errors = {};\n void this.open();\n };\n\n /** Renders the reset form first, so every open starts empty, then shows the modal. */\n private async open(): Promise<void> {\n await this.updateComplete;\n this.formEl?.reset();\n if (this.dialogEl && !this.dialogEl.open) {\n this.dialogEl.showModal();\n setModalOpen(this, true);\n }\n this.renderRoot.querySelector<HTMLElement>('input')?.focus();\n }\n\n private close(): void {\n this.dialogEl?.close();\n }\n\n /** Runs on every close (Esc, the close button, the backdrop): focus goes back to the opener. */\n private onDialogClose(): void {\n // The close event is queued, so a quick reopen may already have shown the dialog again.\n if (!this.dialogEl?.open) setModalOpen(this, false);\n const trigger = this.trigger;\n this.trigger = null;\n trigger?.focus();\n }\n\n private onBackdropClick(event: MouseEvent): void {\n if (event.target === this.dialogEl) this.close();\n }\n\n private fieldValue(form: HTMLFormElement, name: string): string {\n const field = form.elements.namedItem(name);\n return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement ? field.value.trim() : '';\n }\n\n private validate(form: HTMLFormElement): Partial<Record<FieldName, string>> {\n const errors: Partial<Record<FieldName, string>> = {};\n const check = (name: FieldName, label: string, emailCheck = false) => {\n const field = form.elements.namedItem(name);\n if (!(field instanceof HTMLInputElement)) return;\n const blank = name !== 'consent' && field.value.trim() === '';\n if (field.validity.valueMissing || blank) errors[name] = name === 'consent' ? 'Please accept to continue.' : `${label} is required.`;\n else if (emailCheck && field.validity.typeMismatch) errors[name] = 'Enter a valid email address.';\n };\n check('name', this.nameLabel);\n check('email', this.emailLabel, true);\n check('company', this.companyLabel);\n if (this.consentLabel) check('consent', this.consentLabel);\n return errors;\n }\n\n private onSubmit(event: SubmitEvent): void {\n event.preventDefault();\n const form = event.currentTarget as HTMLFormElement;\n const errors = this.validate(form);\n this.errors = errors;\n const firstInvalid = (['name', 'email', 'company', 'consent'] as FieldName[]).find(name => errors[name]);\n if (firstInvalid) {\n this.renderRoot.querySelector<HTMLElement>(`#quote-${firstInvalid}`)?.focus();\n return;\n }\n\n const values = {\n name: this.fieldValue(form, 'name'),\n email: this.fieldValue(form, 'email'),\n company: this.fieldValue(form, 'company'),\n count: this.fieldValue(form, 'count'),\n message: this.fieldValue(form, 'message'),\n consent: (form.elements.namedItem('consent') as HTMLInputElement | null)?.checked ?? false\n };\n const option = this.options.find(item => item.id === this.selected);\n const optionLabel = option?.label ?? this.selected;\n\n if (this.mailto) {\n window.location.href = buildQuoteMailto({\n to: this.mailto,\n subject: fillTemplate(this.subject, {\n option: option?.subjectLabel || optionLabel,\n company: values.company,\n name: values.name\n }),\n optionLabel,\n fields: [\n { label: this.nameLabel, value: values.name },\n { label: this.emailLabel, value: values.email },\n { label: this.companyLabel, value: values.company },\n { label: this.countLabel, value: values.count },\n { label: this.messageLabel, value: values.message }\n ]\n });\n }\n\n this.submittedOption = optionLabel;\n this.submitted = true;\n this.dispatchEvent(\n new CustomEvent<QuoteSubmitEventDetail>('quote-submit', {\n detail: { option: this.selected, ...values },\n bubbles: true,\n composed: true\n })\n );\n // The form (and the focused submit button) is replaced by the success state: keep focus in the dialog.\n void this.updateComplete.then(() => this.renderRoot.querySelector<HTMLElement>('.done')?.focus());\n }\n\n private renderField(\n name: FieldName | 'count',\n label: string,\n extra: { required?: boolean; autocomplete?: string; placeholder?: string; inputmode?: string; type?: string } = {}\n ) {\n const error = name === 'count' ? undefined : this.errors[name];\n return html`<div class=\"field\">\n <label class=\"label\" for=\"quote-${name}\">${label}${extra.required ? html`<span aria-hidden=\"true\"> *</span>` : nothing}</label>\n <input\n class=\"input\"\n id=\"quote-${name}\"\n name=${name}\n type=${extra.type ?? 'text'}\n part=\"input\"\n autocomplete=${extra.autocomplete ?? 'off'}\n inputmode=${extra.inputmode ?? nothing}\n placeholder=${extra.placeholder || nothing}\n ?required=${extra.required}\n aria-invalid=${error ? 'true' : 'false'}\n aria-describedby=${error ? `quote-${name}-error` : nothing}\n />\n ${error ? html`<p class=\"error\" id=\"quote-${name}-error\">${error}</p>` : nothing}\n </div>`;\n }\n\n /** One sentence listing every failing field, for the live region (\"\" when valid). */\n private errorSummary(): string {\n const messages = (['name', 'email', 'company', 'consent'] as FieldName[])\n .map(name => this.errors[name])\n .filter((message): message is string => Boolean(message));\n if (messages.length === 0) return '';\n const count = messages.length;\n return `Please fix ${count} ${count === 1 ? 'field' : 'fields'}: ${messages.join(' ')}`;\n }\n\n private renderForm() {\n const consentError = this.errors.consent;\n return html`<form part=\"form\" novalidate @submit=${this.onSubmit}>\n <div class=\"sr-only\" aria-live=\"polite\">${this.errorSummary()}</div>\n <fieldset class=\"options\">\n <legend class=\"legend\">${this.optionsLabel}</legend>\n <div class=\"option-list\">\n ${this.options.map(\n option => html`<label class=\"option ${this.selected === option.id ? 'selected' : ''}\">\n <input\n type=\"radio\"\n name=\"option\"\n value=${option.id}\n ?checked=${this.selected === option.id}\n @change=${() => (this.selected = option.id)}\n />\n <span class=\"option-body\">\n <span class=\"option-label\">${option.label}</span>\n ${option.hint ? html`<span class=\"option-hint\">${option.hint}</span>` : nothing}\n </span>\n </label>`\n )}\n </div>\n </fieldset>\n <div class=\"fields\">\n ${this.renderField('name', this.nameLabel, { required: true, autocomplete: 'name', placeholder: this.namePlaceholder })}\n ${this.renderField('email', this.emailLabel, { required: true, autocomplete: 'email', type: 'email', placeholder: this.emailPlaceholder })}\n ${this.renderField('company', this.companyLabel, { required: true, autocomplete: 'organization', placeholder: this.companyPlaceholder })}\n ${this.renderField('count', this.countLabel, { inputmode: 'numeric', placeholder: this.countPlaceholder })}\n </div>\n <div class=\"field\">\n <label class=\"label\" for=\"quote-message\">${this.messageLabel} <span class=\"optional\">(optional)</span></label>\n <textarea class=\"input\" id=\"quote-message\" name=\"message\" rows=\"3\" placeholder=${this.messagePlaceholder || nothing}></textarea>\n </div>\n ${this.consentLabel ? this.renderConsent(consentError) : nothing}\n <button class=\"submit\" part=\"submit\" type=\"submit\">${this.submitLabel}</button>\n </form>`;\n }\n\n private renderConsent(consentError: string | undefined) {\n return html`<div class=\"field\">\n <label class=\"consent\">\n <input\n type=\"checkbox\"\n id=\"quote-consent\"\n name=\"consent\"\n required\n aria-invalid=${consentError ? 'true' : 'false'}\n aria-describedby=${consentError ? 'quote-consent-error' : nothing}\n />\n <span>${this.consentLabel}<span aria-hidden=\"true\"> *</span></span>\n </label>\n ${consentError ? html`<p class=\"error\" id=\"quote-consent-error\">${consentError}</p>` : nothing}\n </div>`;\n }\n\n private async copyAddress(): Promise<void> {\n try {\n await navigator.clipboard.writeText(this.mailto);\n this.copied = true;\n } catch {\n // Clipboard blocked: the address is still shown as a link to select by hand.\n }\n }\n\n /** The mailto note with `{email}` as a link to the address. */\n private renderMailtoNote() {\n if (!this.mailto || !this.mailtoNote) return nothing;\n const [before, ...rest] = this.mailtoNote.split('{email}');\n const link = html`<a href=\"mailto:${this.mailto}\">${this.mailto}</a>`;\n return html`<p class=\"mailto-note\" part=\"mailto-note\">\n ${before}${rest.length ? html`${link}${rest.join(this.mailto)}` : nothing}\n </p>`;\n }\n\n private renderSuccess() {\n const showCopy = Boolean(this.mailto && this.mailtoNote);\n return html`<div class=\"success\" role=\"status\">\n <span class=\"success-icon\" aria-hidden=\"true\">${SUCCESS_ICON}</span>\n <h3 class=\"success-title\">${this.successTitle}</h3>\n <p class=\"success-text\">${this.successText.replaceAll('{option}', this.submittedOption)}</p>\n ${this.renderMailtoNote()}\n <div class=\"success-actions\">\n ${showCopy\n ? html`<button class=\"copy\" part=\"copy\" type=\"button\" @click=${this.copyAddress}>\n ${this.copied ? this.copiedLabel : this.copyLabel}\n </button>`\n : nothing}\n <button class=\"done\" part=\"close\" type=\"button\" @click=${this.close}>${this.closeLabel}</button>\n </div>\n </div>`;\n }\n\n render() {\n return html`\n <dialog\n part=\"dialog\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"quote-title\"\n @close=${this.onDialogClose}\n @click=${this.onBackdropClick}\n >\n <div class=\"panel\">\n <div class=\"header\">\n <div class=\"heading\">\n <h2 class=\"title\" id=\"quote-title\">${this.title}</h2>\n ${this.subtitle ? html`<p class=\"subtitle\">${this.subtitle}</p>` : nothing}\n </div>\n <button class=\"close\" part=\"close\" type=\"button\" aria-label=${this.closeLabel} @click=${this.close}>\n ${CLOSE_ICON}\n </button>\n </div>\n ${this.submitted ? this.renderSuccess() : this.renderForm()}\n </div>\n </dialog>\n `;\n }\n}\n","/** One labelled line of a `mailto:` body. */\nexport interface MailtoField {\n label: string;\n value: string;\n}\n\nexport interface MailtoInput {\n /** Recipient address. */\n to: string;\n subject: string;\n /** Written into the body as one `Label: value` line each, in order. */\n fields: MailtoField[];\n /**\n * Longest the percent-encoded body may be (default 1800). Mail apps and browsers cut off long\n * `mailto:` URLs, so the field picked by `truncateField` is shortened with \"…\" until the body fits.\n */\n maxBodyLength?: number;\n /** Index of the field to shorten when the body is too long (default: the last field). */\n truncateField?: number;\n}\n\nexport const DEFAULT_MAILTO_BODY_LIMIT = 1800;\n\nconst ELLIPSIS = '…';\n\nfunction bodyText(fields: MailtoField[]): string {\n return fields.map(field => `${field.label}: ${field.value.trim() || '-'}`).join('\\n');\n}\n\n/**\n * Builds a `mailto:` URL whose body is plain text: one `Label: value` line per field (an empty value\n * is written as `-`). Subject and body are percent-encoded, so line breaks and symbols survive the\n * URL. When the encoded body is longer than `maxBodyLength`, one field (the last, unless\n * `truncateField` says otherwise) is cut short, on a character boundary, and ends in \"…\".\n */\nexport function buildMailto({ to, subject, fields, maxBodyLength = DEFAULT_MAILTO_BODY_LIMIT, truncateField }: MailtoInput): string {\n const url = (body: string) => `mailto:${to}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;\n const full = bodyText(fields);\n if (encodeURIComponent(full).length <= maxBodyLength || fields.length === 0) return url(full);\n\n const target = Math.min(Math.max(truncateField ?? fields.length - 1, 0), fields.length - 1);\n // Array.from splits by code point, so a cut never leaves half of a surrogate pair.\n const chars = Array.from(fields[target].value.trim());\n const withLength = (length: number) =>\n bodyText(fields.map((field, i) => (i === target ? { ...field, value: chars.slice(0, length).join('').trimEnd() + ELLIPSIS } : field)));\n\n // Longest prefix of the field that still fits (binary search; 0 keeps just the ellipsis).\n let low = 0;\n let high = chars.length;\n while (low < high) {\n const mid = Math.ceil((low + high) / 2);\n if (encodeURIComponent(withLength(mid)).length <= maxBodyLength) low = mid;\n else high = mid - 1;\n }\n return url(withLength(low));\n}\n","import { buildMailto, type MailtoField } from '../shared/mailto';\n\n/** One labelled line of the request e-mail. */\nexport type QuoteMailField = MailtoField;\n\nexport interface QuoteMailtoInput {\n /** Recipient address. */\n to: string;\n subject: string;\n /** Human label of the chosen option, written into the body. */\n optionLabel: string;\n fields: QuoteMailField[];\n /** Longest the encoded body may be; the last field is shortened with \"…\" to fit (default 1800). */\n maxBodyLength?: number;\n}\n\n/**\n * Builds a `mailto:` URL whose body is plain text: the chosen option, then one `Label: value` line\n * per field (see `buildMailto`). Subject and body are percent-encoded, so line breaks and symbols\n * survive the URL. An empty value is written as `-`.\n */\nexport function buildQuoteMailto({ to, subject, optionLabel, fields, maxBodyLength }: QuoteMailtoInput): string {\n return buildMailto({ to, subject, fields: [{ label: 'Option', value: optionLabel }, ...fields], maxBodyLength });\n}\n\n/** Fills `{name}` placeholders in a subject or text line; unknown names are left as they are. */\nexport function fillTemplate(template: string, values: Record<string, string>): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, key: string) => (Object.hasOwn(values, key) ? values[key] : match));\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,32 @@
1
+ // src/shared/cta-label.ts
2
+ import { css, html, nothing } from "lit";
3
+ var SHORT_LABEL_MAX_WIDTH = 479;
4
+ var ctaLabelStyles = css`
5
+ .label-short {
6
+ display: none;
7
+ }
8
+
9
+ @media (max-width: ${SHORT_LABEL_MAX_WIDTH}px) {
10
+ .label-short {
11
+ display: inline;
12
+ }
13
+
14
+ .label-long:has(+ .label-short) {
15
+ display: none;
16
+ }
17
+ }
18
+ `;
19
+ function renderCtaLabel(label, short) {
20
+ if (!short || short === label) return label;
21
+ return html`<span class="label-long">${label}</span><span class="label-short" aria-hidden="true">${short}</span>`;
22
+ }
23
+ function ctaAriaLabel(label, short, suffix = "") {
24
+ return short && short !== label ? `${label}${suffix}` : nothing;
25
+ }
26
+
27
+ export {
28
+ ctaLabelStyles,
29
+ renderCtaLabel,
30
+ ctaAriaLabel
31
+ };
32
+ //# sourceMappingURL=chunk-BEWX3E6P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/cta-label.ts"],"sourcesContent":["import { css, html, nothing, type TemplateResult } from 'lit';\n\n/** Phones narrower than this show the short CTA label. */\nexport const SHORT_LABEL_MAX_WIDTH = 479;\n\n/** Styles for the long/short label pair; include them in any component that uses `renderCtaLabel`. */\nexport const ctaLabelStyles = css`\n .label-short {\n display: none;\n }\n\n @media (max-width: ${SHORT_LABEL_MAX_WIDTH}px) {\n .label-short {\n display: inline;\n }\n\n .label-long:has(+ .label-short) {\n display: none;\n }\n }\n`;\n\n/**\n * The text of a CTA. With a `short` label, both are rendered and CSS shows the short one on phones\n * (< 480px), so the button fits on one line. The link then needs `ariaLabel` (the full wording).\n */\nexport function renderCtaLabel(label: string, short: string): TemplateResult | string {\n if (!short || short === label) return label;\n return html`<span class=\"label-long\">${label}</span><span class=\"label-short\" aria-hidden=\"true\">${short}</span>`;\n}\n\n/** `aria-label` that keeps the full wording while the short label is visible; `nothing` otherwise. */\nexport function ctaAriaLabel(label: string, short: string, suffix = ''): string | typeof nothing {\n return short && short !== label ? `${label}${suffix}` : nothing;\n}\n"],"mappings":";AAAA,SAAS,KAAK,MAAM,eAAoC;AAGjD,IAAM,wBAAwB;AAG9B,IAAM,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,uBAKP,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAerC,SAAS,eAAe,OAAe,OAAwC;AACpF,MAAI,CAAC,SAAS,UAAU,MAAO,QAAO;AACtC,SAAO,gCAAgC,KAAK,uDAAuD,KAAK;AAC1G;AAGO,SAAS,aAAa,OAAe,OAAe,SAAS,IAA6B;AAC/F,SAAO,SAAS,UAAU,QAAQ,GAAG,KAAK,GAAG,MAAM,KAAK;AAC1D;","names":[]}
@@ -0,0 +1,275 @@
1
+ // src/contact-form/contact-form-client.ts
2
+ var CONTACT_FIELD_NAMES = ["name", "email", "company", "phone", "topic", "message", "consent"];
3
+ var CONTACT_LIMITS = {
4
+ nameMax: 100,
5
+ emailMax: 254,
6
+ companyMax: 120,
7
+ phoneMax: 30,
8
+ messageMin: 10,
9
+ messageMax: 2e3
10
+ };
11
+ var COUNTER_SHOW_RATIO = 0.8;
12
+ var ERROR_SUMMARY_MIN = 3;
13
+ var MAILTO_PATTERN = /^[^\s@<>"'`\\]+@[^\s@<>"'`\\]+\.[^\s@<>"'`\\]+$/;
14
+ function safeMailto(address) {
15
+ const trimmed = address.trim();
16
+ return MAILTO_PATTERN.test(trimmed) ? trimmed : void 0;
17
+ }
18
+ function splitMailto(note) {
19
+ const pieces = [];
20
+ note.split("{mailto}").forEach((part, index) => {
21
+ if (index > 0) pieces.push(null);
22
+ if (part !== "") pieces.push(part);
23
+ });
24
+ return pieces;
25
+ }
26
+ function counterBand(length, max) {
27
+ if (length > max) return 3;
28
+ if (length >= max * 0.9) return 2;
29
+ if (length >= max * COUNTER_SHOW_RATIO) return 1;
30
+ return 0;
31
+ }
32
+ var CONTACT_TIMEOUT_MS = 15e3;
33
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
34
+ var PHONE_PATTERN = /^[0-9 +\-().]+$/;
35
+ var RETRY_AFTER_PATTERN = /^\d+(\.\d+)?$/;
36
+ var RETRY_AFTER_MIN_SECONDS = 1;
37
+ var RETRY_AFTER_MAX_SECONDS = 3600;
38
+ var REFERENCE_MAX_LENGTH = 64;
39
+ var DEFAULT_ERROR_MESSAGES = {
40
+ required: "{field} is required.",
41
+ tooLong: "{field} must be {max} characters or fewer.",
42
+ invalidEmail: "Enter a valid email address.",
43
+ invalidPhone: "Use only digits, spaces and + - ( ) . in the phone number.",
44
+ unknownTopic: "Choose a topic from the list.",
45
+ messageTooShort: "Your message must be at least {min} characters.",
46
+ messageTooLong: "Your message must be {max} characters or fewer.",
47
+ consentRequired: "Please accept to continue."
48
+ };
49
+ function codePointLength(text) {
50
+ return [...text].length;
51
+ }
52
+ function fillTemplate(template, values) {
53
+ return template.replace(/\{(\w+)\}/g, (placeholder, key) => Object.hasOwn(values, key) ? String(values[key]) : placeholder);
54
+ }
55
+ function validateContact(values, options) {
56
+ const { labels, topics, messages } = options;
57
+ const errors = {};
58
+ const name = values.name.trim();
59
+ const email = values.email.trim();
60
+ const company = values.company.trim();
61
+ const phone = values.phone.trim();
62
+ const topic = values.topic.trim();
63
+ const message = values.message.trim();
64
+ const required = (field) => fillTemplate(messages.required, { field: labels[field] });
65
+ const tooLong = (field, max) => fillTemplate(messages.tooLong, { field: labels[field], max });
66
+ if (name === "") errors.name = required("name");
67
+ else if (codePointLength(name) > CONTACT_LIMITS.nameMax) errors.name = tooLong("name", CONTACT_LIMITS.nameMax);
68
+ if (email === "") errors.email = required("email");
69
+ else if (codePointLength(email) > CONTACT_LIMITS.emailMax) errors.email = tooLong("email", CONTACT_LIMITS.emailMax);
70
+ else if (!EMAIL_PATTERN.test(email)) errors.email = messages.invalidEmail;
71
+ if (company === "") errors.company = required("company");
72
+ else if (codePointLength(company) > CONTACT_LIMITS.companyMax) errors.company = tooLong("company", CONTACT_LIMITS.companyMax);
73
+ if (phone !== "") {
74
+ if (codePointLength(phone) > CONTACT_LIMITS.phoneMax) errors.phone = tooLong("phone", CONTACT_LIMITS.phoneMax);
75
+ else if (!PHONE_PATTERN.test(phone)) errors.phone = messages.invalidPhone;
76
+ }
77
+ if (topic === "") errors.topic = required("topic");
78
+ else if (topics.length > 0 && !topics.some((item) => item.value === topic)) errors.topic = messages.unknownTopic;
79
+ const length = codePointLength(message);
80
+ if (length === 0) errors.message = required("message");
81
+ else if (length < CONTACT_LIMITS.messageMin) errors.message = fillTemplate(messages.messageTooShort, { min: CONTACT_LIMITS.messageMin });
82
+ else if (length > CONTACT_LIMITS.messageMax) errors.message = fillTemplate(messages.messageTooLong, { max: CONTACT_LIMITS.messageMax });
83
+ if (!values.consent) errors.consent = messages.consentRequired;
84
+ return errors;
85
+ }
86
+ function firstInvalidField(errors) {
87
+ return CONTACT_FIELD_NAMES.find((name) => errors[name] !== void 0);
88
+ }
89
+ function buildContactPayload(values, meta) {
90
+ if (!values.consent) throw new Error("A contact request needs consent.");
91
+ return {
92
+ name: values.name.trim(),
93
+ email: values.email.trim(),
94
+ company: values.company.trim(),
95
+ phone: values.phone.trim(),
96
+ topic: values.topic.trim(),
97
+ message: values.message.trim(),
98
+ consent: true,
99
+ page: meta.page,
100
+ locale: meta.locale,
101
+ submittedAt: meta.now.toISOString(),
102
+ website: values.website ?? ""
103
+ };
104
+ }
105
+ function isPlainObject(value) {
106
+ return typeof value === "object" && value !== null && !Array.isArray(value);
107
+ }
108
+ function normalizeTopics(input) {
109
+ if (!Array.isArray(input)) return [];
110
+ const seen = /* @__PURE__ */ new Set();
111
+ const topics = [];
112
+ for (const item of input) {
113
+ if (!isPlainObject(item)) continue;
114
+ const value = typeof item.value === "string" ? item.value.trim() : "";
115
+ if (value === "" || seen.has(value)) continue;
116
+ seen.add(value);
117
+ const label = typeof item.label === "string" && item.label.trim() !== "" ? item.label.trim() : value;
118
+ const group = typeof item.group === "string" && item.group.trim() !== "" ? item.group.trim() : void 0;
119
+ topics.push(group === void 0 ? { value, label } : { value, label, group });
120
+ }
121
+ return topics;
122
+ }
123
+ function parseTopicsAttribute(text) {
124
+ if (text === null || text.trim() === "") return [];
125
+ try {
126
+ return normalizeTopics(JSON.parse(text));
127
+ } catch {
128
+ return [];
129
+ }
130
+ }
131
+ function groupTopics(topics) {
132
+ const groups = [];
133
+ for (const topic of topics) {
134
+ let group = groups.find((item) => item.group === topic.group);
135
+ if (!group) {
136
+ group = topic.group === void 0 ? { topics: [] } : { group: topic.group, topics: [] };
137
+ groups.push(group);
138
+ }
139
+ group.topics.push(topic);
140
+ }
141
+ return groups;
142
+ }
143
+ function readContactQuery(search) {
144
+ const query = { product: null, topic: null };
145
+ new URLSearchParams(search).forEach((value, key) => {
146
+ const name = key.toLowerCase();
147
+ if (name === "product" && query.product === null) query.product = value;
148
+ if (name === "topic" && query.topic === null) query.topic = value;
149
+ });
150
+ return query;
151
+ }
152
+ function resolvePreselectedTopic(topics, options) {
153
+ const findByName = (candidate) => {
154
+ const wanted = candidate?.trim().toLowerCase();
155
+ if (!wanted) return void 0;
156
+ return topics.find((item) => item.value.toLowerCase() === wanted);
157
+ };
158
+ const byProduct = findByName(options.product);
159
+ if (byProduct) return { value: byProduct.value, fromQuery: true };
160
+ const byTopic = findByName(options.topic);
161
+ if (byTopic) return { value: byTopic.value, fromQuery: true };
162
+ const wantedDefault = options.defaultTopic?.trim();
163
+ const byDefault = wantedDefault ? topics.find((item) => item.value === wantedDefault) : void 0;
164
+ return { value: byDefault?.value ?? "", fromQuery: false };
165
+ }
166
+ function parseRetryAfter(raw) {
167
+ if (raw === null) return void 0;
168
+ const trimmed = raw.trim();
169
+ if (!RETRY_AFTER_PATTERN.test(trimmed)) return void 0;
170
+ const seconds = Math.ceil(Number(trimmed));
171
+ return Math.min(RETRY_AFTER_MAX_SECONDS, Math.max(RETRY_AFTER_MIN_SECONDS, seconds));
172
+ }
173
+ function parseJsonObject(text) {
174
+ try {
175
+ const value = JSON.parse(text);
176
+ return isPlainObject(value) ? value : void 0;
177
+ } catch {
178
+ return void 0;
179
+ }
180
+ }
181
+ function parseReference(value) {
182
+ if (typeof value !== "string") return void 0;
183
+ const trimmed = value.trim();
184
+ return trimmed !== "" && trimmed.length <= REFERENCE_MAX_LENGTH ? trimmed : void 0;
185
+ }
186
+ function unavailable() {
187
+ return { kind: "unavailable" };
188
+ }
189
+ function interpretValidation(body) {
190
+ const raw = body.errors;
191
+ if (!isPlainObject(raw)) return unavailable();
192
+ const fieldNames = CONTACT_FIELD_NAMES;
193
+ const errors = {};
194
+ let formMessage;
195
+ for (const [key, message] of Object.entries(raw)) {
196
+ if (typeof message !== "string" || message.trim() === "") continue;
197
+ if (fieldNames.includes(key)) errors[key] ??= message;
198
+ else formMessage ??= message;
199
+ }
200
+ if (Object.keys(errors).length === 0 && formMessage === void 0) return unavailable();
201
+ return formMessage === void 0 ? { kind: "validation", errors } : { kind: "validation", errors, formMessage };
202
+ }
203
+ function interpretContactResponse(status, retryAfter, bodyText) {
204
+ if (status === 429) {
205
+ const retryAfterSeconds = parseRetryAfter(retryAfter);
206
+ return retryAfterSeconds === void 0 ? { kind: "rate_limited" } : { kind: "rate_limited", retryAfterSeconds };
207
+ }
208
+ const body = parseJsonObject(bodyText);
209
+ if (!body) return unavailable();
210
+ if (status === 200 || status === 201) {
211
+ if (body.ok !== true) return unavailable();
212
+ const reference = parseReference(body.reference);
213
+ return reference === void 0 ? { kind: "sent" } : { kind: "sent", reference };
214
+ }
215
+ if (status === 400) return interpretValidation(body);
216
+ return unavailable();
217
+ }
218
+ function isSubmittableEndpoint(endpoint) {
219
+ if (endpoint.trim() === "") return false;
220
+ try {
221
+ const url = new URL(endpoint);
222
+ return url.protocol === "https:" || url.protocol === "http:";
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+ async function submitContact(options) {
228
+ const { endpoint, payload, signal, timeoutMs = CONTACT_TIMEOUT_MS, fetchImpl = globalThis.fetch } = options;
229
+ if (!isSubmittableEndpoint(endpoint) || signal?.aborted) return unavailable();
230
+ const controller = new AbortController();
231
+ const abort = () => controller.abort();
232
+ const timer = setTimeout(abort, timeoutMs);
233
+ signal?.addEventListener("abort", abort, { once: true });
234
+ try {
235
+ const response = await fetchImpl(endpoint, {
236
+ method: "POST",
237
+ mode: "cors",
238
+ credentials: "omit",
239
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
240
+ body: JSON.stringify(payload),
241
+ signal: controller.signal
242
+ });
243
+ const text = await response.text();
244
+ return interpretContactResponse(response.status, response.headers.get("Retry-After"), text);
245
+ } catch {
246
+ return unavailable();
247
+ } finally {
248
+ clearTimeout(timer);
249
+ signal?.removeEventListener("abort", abort);
250
+ }
251
+ }
252
+
253
+ export {
254
+ CONTACT_FIELD_NAMES,
255
+ CONTACT_LIMITS,
256
+ COUNTER_SHOW_RATIO,
257
+ ERROR_SUMMARY_MIN,
258
+ safeMailto,
259
+ splitMailto,
260
+ counterBand,
261
+ DEFAULT_ERROR_MESSAGES,
262
+ codePointLength,
263
+ fillTemplate,
264
+ validateContact,
265
+ firstInvalidField,
266
+ buildContactPayload,
267
+ normalizeTopics,
268
+ parseTopicsAttribute,
269
+ groupTopics,
270
+ readContactQuery,
271
+ resolvePreselectedTopic,
272
+ parseRetryAfter,
273
+ submitContact
274
+ };
275
+ //# sourceMappingURL=chunk-CZUGPKXV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contact-form/contact-form-client.ts"],"sourcesContent":["/**\n * The DOM-free logic behind `st-contact-form`: the client-side checks, the request body, the call itself\n * and the reading of its answer. The server's rules stay authoritative; the checks here only give the\n * visitor an answer before a request is made. Kept apart from `ContactForm.ts` so every rule and every\n * response case can be unit-tested without a browser.\n */\n\n/** The fields, in the order the form shows them. Focus moves through them in this order. */\nexport const CONTACT_FIELD_NAMES = ['name', 'email', 'company', 'phone', 'topic', 'message', 'consent'] as const;\n\nexport type ContactFieldName = (typeof CONTACT_FIELD_NAMES)[number];\n\n/** Length limits, counted in Unicode code points. They match the server's; change both together. */\nexport const CONTACT_LIMITS = {\n nameMax: 100,\n emailMax: 254,\n companyMax: 120,\n phoneMax: 30,\n messageMin: 10,\n messageMax: 2000\n} as const;\n\n/** The message counter shows from this share of the limit. */\nexport const COUNTER_SHOW_RATIO = 0.8;\n\n/** Failing fields from which the form shows a summary (with links) instead of focusing the first field. */\nexport const ERROR_SUMMARY_MIN = 3;\n\nconst MAILTO_PATTERN = /^[^\\s@<>\"'`\\\\]+@[^\\s@<>\"'`\\\\]+\\.[^\\s@<>\"'`\\\\]+$/;\n\n/** The address when it is safe to put in a `mailto:` link, else `undefined`. */\nexport function safeMailto(address: string): string | undefined {\n const trimmed = address.trim();\n return MAILTO_PATTERN.test(trimmed) ? trimmed : undefined;\n}\n\n/** Splits a note at its `{mailto}` placeholders: the text pieces, with `null` where the link goes. */\nexport function splitMailto(note: string): (string | null)[] {\n const pieces: (string | null)[] = [];\n note.split('{mailto}').forEach((part, index) => {\n if (index > 0) pieces.push(null);\n if (part !== '') pieces.push(part);\n });\n return pieces;\n}\n\n/** 0 below 80% of the limit, 1 from 80%, 2 from 90%, 3 once over. Screen readers hear a change of band, not every key. */\nexport function counterBand(length: number, max: number): 0 | 1 | 2 | 3 {\n if (length > max) return 3;\n if (length >= max * 0.9) return 2;\n if (length >= max * COUNTER_SHOW_RATIO) return 1;\n return 0;\n}\n\n/** How long a submission may take before the form reports that it could not be sent. */\nexport const CONTACT_TIMEOUT_MS = 15_000;\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst PHONE_PATTERN = /^[0-9 +\\-().]+$/;\nconst RETRY_AFTER_PATTERN = /^\\d+(\\.\\d+)?$/;\nconst RETRY_AFTER_MIN_SECONDS = 1;\nconst RETRY_AFTER_MAX_SECONDS = 3600;\nconst REFERENCE_MAX_LENGTH = 64;\n\n/** One choice in the topic list. `group` puts it under an `<optgroup>` with that label. */\nexport interface ContactTopic {\n value: string;\n label: string;\n group?: string;\n}\n\n/** The raw values read from the form. Strings are as typed; `consent` is the checkbox. */\nexport interface ContactValues {\n name: string;\n email: string;\n company: string;\n phone: string;\n topic: string;\n message: string;\n consent: boolean;\n /** The honeypot input. Real visitors leave it empty. */\n website?: string;\n}\n\n/** The visible label of each text field, used in the \"... is required.\" messages. */\nexport type ContactFieldLabels = Record<'name' | 'email' | 'company' | 'phone' | 'topic' | 'message', string>;\n\n/**\n * The client-side messages. `{field}`, `{max}`, `{min}` are filled in. The server's messages are shown\n * as the server sends them, so these are the only text the component writes itself.\n */\nexport interface ContactErrorMessages {\n required: string;\n tooLong: string;\n invalidEmail: string;\n invalidPhone: string;\n unknownTopic: string;\n messageTooShort: string;\n messageTooLong: string;\n consentRequired: string;\n}\n\nexport const DEFAULT_ERROR_MESSAGES: ContactErrorMessages = {\n required: '{field} is required.',\n tooLong: '{field} must be {max} characters or fewer.',\n invalidEmail: 'Enter a valid email address.',\n invalidPhone: 'Use only digits, spaces and + - ( ) . in the phone number.',\n unknownTopic: 'Choose a topic from the list.',\n messageTooShort: 'Your message must be at least {min} characters.',\n messageTooLong: 'Your message must be {max} characters or fewer.',\n consentRequired: 'Please accept to continue.'\n};\n\n/** Field name to message, for the fields that have one. */\nexport type ContactErrors = Partial<Record<ContactFieldName, string>>;\n\nexport interface ContactValidationOptions {\n labels: ContactFieldLabels;\n topics: readonly ContactTopic[];\n messages: ContactErrorMessages;\n}\n\n/** The request body. Exactly these keys, always. */\nexport interface ContactPayload {\n name: string;\n email: string;\n company: string;\n phone: string;\n topic: string;\n message: string;\n consent: true;\n page: string;\n locale: string;\n submittedAt: string;\n website: string;\n}\n\n/** What a submission came back with, already interpreted. */\nexport type ContactSubmitResult =\n | { kind: 'sent'; reference?: string }\n | { kind: 'validation'; errors: ContactErrors; formMessage?: string }\n | { kind: 'rate_limited'; retryAfterSeconds?: number }\n | { kind: 'unavailable' };\n\n/** The number of Unicode code points, so an emoji counts as one character. */\nexport function codePointLength(text: string): number {\n return [...text].length;\n}\n\n/** Replaces `{name}` placeholders in one pass. Substituted text is never scanned again. */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{(\\w+)\\}/g, (placeholder, key: string) => (Object.hasOwn(values, key) ? String(values[key]) : placeholder));\n}\n\n/** Checks the values the way the server does. Only failing fields appear in the result. */\nexport function validateContact(values: ContactValues, options: ContactValidationOptions): ContactErrors {\n const { labels, topics, messages } = options;\n const errors: ContactErrors = {};\n const name = values.name.trim();\n const email = values.email.trim();\n const company = values.company.trim();\n const phone = values.phone.trim();\n const topic = values.topic.trim();\n const message = values.message.trim();\n const required = (field: keyof ContactFieldLabels) => fillTemplate(messages.required, { field: labels[field] });\n const tooLong = (field: keyof ContactFieldLabels, max: number) => fillTemplate(messages.tooLong, { field: labels[field], max });\n\n if (name === '') errors.name = required('name');\n else if (codePointLength(name) > CONTACT_LIMITS.nameMax) errors.name = tooLong('name', CONTACT_LIMITS.nameMax);\n\n if (email === '') errors.email = required('email');\n else if (codePointLength(email) > CONTACT_LIMITS.emailMax) errors.email = tooLong('email', CONTACT_LIMITS.emailMax);\n else if (!EMAIL_PATTERN.test(email)) errors.email = messages.invalidEmail;\n\n if (company === '') errors.company = required('company');\n else if (codePointLength(company) > CONTACT_LIMITS.companyMax) errors.company = tooLong('company', CONTACT_LIMITS.companyMax);\n\n if (phone !== '') {\n if (codePointLength(phone) > CONTACT_LIMITS.phoneMax) errors.phone = tooLong('phone', CONTACT_LIMITS.phoneMax);\n else if (!PHONE_PATTERN.test(phone)) errors.phone = messages.invalidPhone;\n }\n\n if (topic === '') errors.topic = required('topic');\n else if (topics.length > 0 && !topics.some(item => item.value === topic)) errors.topic = messages.unknownTopic;\n\n const length = codePointLength(message);\n if (length === 0) errors.message = required('message');\n else if (length < CONTACT_LIMITS.messageMin) errors.message = fillTemplate(messages.messageTooShort, { min: CONTACT_LIMITS.messageMin });\n else if (length > CONTACT_LIMITS.messageMax) errors.message = fillTemplate(messages.messageTooLong, { max: CONTACT_LIMITS.messageMax });\n\n if (!values.consent) errors.consent = messages.consentRequired;\n return errors;\n}\n\n/** The first failing field, in form order. */\nexport function firstInvalidField(errors: ContactErrors): ContactFieldName | undefined {\n return CONTACT_FIELD_NAMES.find(name => errors[name] !== undefined);\n}\n\n/**\n * Builds the request body from checked values: trimmed text, `consent` as the literal `true`, and the\n * honeypot value as typed (normally empty). Throws if consent is missing, which validation prevents.\n */\nexport function buildContactPayload(values: ContactValues, meta: { page: string; locale: string; now: Date }): ContactPayload {\n if (!values.consent) throw new Error('A contact request needs consent.');\n return {\n name: values.name.trim(),\n email: values.email.trim(),\n company: values.company.trim(),\n phone: values.phone.trim(),\n topic: values.topic.trim(),\n message: values.message.trim(),\n consent: true,\n page: meta.page,\n locale: meta.locale,\n submittedAt: meta.now.toISOString(),\n website: values.website ?? ''\n };\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Keeps the usable topics: an object with a non-empty `value` (the first of each value wins). A missing\n * `label` falls back to the value; a blank `group` means no group.\n */\nexport function normalizeTopics(input: unknown): ContactTopic[] {\n if (!Array.isArray(input)) return [];\n const seen = new Set<string>();\n const topics: ContactTopic[] = [];\n for (const item of input) {\n if (!isPlainObject(item)) continue;\n const value = typeof item.value === 'string' ? item.value.trim() : '';\n if (value === '' || seen.has(value)) continue;\n seen.add(value);\n const label = typeof item.label === 'string' && item.label.trim() !== '' ? item.label.trim() : value;\n const group = typeof item.group === 'string' && item.group.trim() !== '' ? item.group.trim() : undefined;\n topics.push(group === undefined ? { value, label } : { value, label, group });\n }\n return topics;\n}\n\n/** Reads the `topics` attribute (a JSON array). Anything unreadable gives an empty list. */\nexport function parseTopicsAttribute(text: string | null): ContactTopic[] {\n if (text === null || text.trim() === '') return [];\n try {\n return normalizeTopics(JSON.parse(text));\n } catch {\n return [];\n }\n}\n\n/** One `<optgroup>` (or the ungrouped options when `group` is undefined), in order of first appearance. */\nexport interface TopicGroup {\n group?: string;\n topics: ContactTopic[];\n}\n\nexport function groupTopics(topics: readonly ContactTopic[]): TopicGroup[] {\n const groups: TopicGroup[] = [];\n for (const topic of topics) {\n let group = groups.find(item => item.group === topic.group);\n if (!group) {\n group = topic.group === undefined ? { topics: [] } : { group: topic.group, topics: [] };\n groups.push(group);\n }\n group.topics.push(topic);\n }\n return groups;\n}\n\nexport interface ContactQuery {\n product: string | null;\n topic: string | null;\n}\n\n/** Reads `product` and `topic` from a query string. Key names are matched without regard to case. */\nexport function readContactQuery(search: string): ContactQuery {\n const query: ContactQuery = { product: null, topic: null };\n new URLSearchParams(search).forEach((value, key) => {\n const name = key.toLowerCase();\n if (name === 'product' && query.product === null) query.product = value;\n if (name === 'topic' && query.topic === null) query.topic = value;\n });\n return query;\n}\n\n/**\n * The topic to preselect. `?product=` wins, then `?topic=`, each only when it names one of the topics\n * (compared without case). Otherwise `defaultTopic` if it is one of the topics. `fromQuery` is true when\n * the page's URL chose it.\n */\nexport function resolvePreselectedTopic(\n topics: readonly ContactTopic[],\n options: { defaultTopic?: string; product?: string | null; topic?: string | null }\n): { value: string; fromQuery: boolean } {\n const findByName = (candidate: string | null | undefined): ContactTopic | undefined => {\n const wanted = candidate?.trim().toLowerCase();\n if (!wanted) return undefined;\n return topics.find(item => item.value.toLowerCase() === wanted);\n };\n const byProduct = findByName(options.product);\n if (byProduct) return { value: byProduct.value, fromQuery: true };\n const byTopic = findByName(options.topic);\n if (byTopic) return { value: byTopic.value, fromQuery: true };\n const wantedDefault = options.defaultTopic?.trim();\n const byDefault = wantedDefault ? topics.find(item => item.value === wantedDefault) : undefined;\n return { value: byDefault?.value ?? '', fromQuery: false };\n}\n\n/**\n * Reads a `Retry-After` header given in seconds. Anything that is not a plain number gives `undefined`\n * (the generic note). Numbers are rounded up and clamped to 1..3600.\n */\nexport function parseRetryAfter(raw: string | null): number | undefined {\n if (raw === null) return undefined;\n const trimmed = raw.trim();\n if (!RETRY_AFTER_PATTERN.test(trimmed)) return undefined;\n const seconds = Math.ceil(Number(trimmed));\n return Math.min(RETRY_AFTER_MAX_SECONDS, Math.max(RETRY_AFTER_MIN_SECONDS, seconds));\n}\n\nfunction parseJsonObject(text: string): Record<string, unknown> | undefined {\n try {\n const value: unknown = JSON.parse(text);\n return isPlainObject(value) ? value : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** A reference is shown as text. Only a short non-empty string is accepted. */\nfunction parseReference(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n return trimmed !== '' && trimmed.length <= REFERENCE_MAX_LENGTH ? trimmed : undefined;\n}\n\nfunction unavailable(): ContactSubmitResult {\n return { kind: 'unavailable' };\n}\n\nfunction interpretValidation(body: Record<string, unknown>): ContactSubmitResult {\n const raw = body.errors;\n if (!isPlainObject(raw)) return unavailable();\n const fieldNames: readonly string[] = CONTACT_FIELD_NAMES;\n const errors: ContactErrors = {};\n let formMessage: string | undefined;\n for (const [key, message] of Object.entries(raw)) {\n if (typeof message !== 'string' || message.trim() === '') continue;\n if (fieldNames.includes(key)) errors[key as ContactFieldName] ??= message;\n else formMessage ??= message;\n }\n if (Object.keys(errors).length === 0 && formMessage === undefined) return unavailable();\n return formMessage === undefined ? { kind: 'validation', errors } : { kind: 'validation', errors, formMessage };\n}\n\n/**\n * Turns a response into the outcome the form shows. Only the documented answers count:\n * 200 or 201 with `ok: true` (a reference is optional), 400 with an `errors` object, 429 (the body is\n * ignored). Any other answer, or any body that is not the expected JSON, is `unavailable`. The response\n * text is never shown.\n */\nexport function interpretContactResponse(status: number, retryAfter: string | null, bodyText: string): ContactSubmitResult {\n if (status === 429) {\n const retryAfterSeconds = parseRetryAfter(retryAfter);\n return retryAfterSeconds === undefined ? { kind: 'rate_limited' } : { kind: 'rate_limited', retryAfterSeconds };\n }\n const body = parseJsonObject(bodyText);\n if (!body) return unavailable();\n if (status === 200 || status === 201) {\n if (body.ok !== true) return unavailable();\n const reference = parseReference(body.reference);\n return reference === undefined ? { kind: 'sent' } : { kind: 'sent', reference };\n }\n if (status === 400) return interpretValidation(body);\n return unavailable();\n}\n\n/** Only `http:` and `https:` URLs are sent to. An empty or relative value is not sendable. */\nexport function isSubmittableEndpoint(endpoint: string): boolean {\n if (endpoint.trim() === '') return false;\n try {\n const url = new URL(endpoint);\n return url.protocol === 'https:' || url.protocol === 'http:';\n } catch {\n return false;\n }\n}\n\n/** The part of `fetch` the submission uses: a string URL in, a response out. */\nexport type ContactFetch = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport interface ContactSubmitOptions {\n endpoint: string;\n payload: ContactPayload;\n /** Aborts the request (the element passes this when it is removed from the page). */\n signal?: AbortSignal;\n timeoutMs?: number;\n fetchImpl?: ContactFetch;\n}\n\n/**\n * Posts the payload as JSON, with no cookies, and interprets the answer. Never throws: a network error,\n * a timeout, an abort or an unsendable endpoint all give `unavailable` (without a request in the last case).\n */\nexport async function submitContact(options: ContactSubmitOptions): Promise<ContactSubmitResult> {\n const { endpoint, payload, signal, timeoutMs = CONTACT_TIMEOUT_MS, fetchImpl = globalThis.fetch } = options;\n if (!isSubmittableEndpoint(endpoint) || signal?.aborted) return unavailable();\n const controller = new AbortController();\n const abort = () => controller.abort();\n const timer = setTimeout(abort, timeoutMs);\n signal?.addEventListener('abort', abort, { once: true });\n try {\n const response = await fetchImpl(endpoint, {\n method: 'POST',\n mode: 'cors',\n credentials: 'omit',\n headers: { 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify(payload),\n signal: controller.signal\n });\n const text = await response.text();\n return interpretContactResponse(response.status, response.headers.get('Retry-After'), text);\n } catch {\n return unavailable();\n } finally {\n clearTimeout(timer);\n signal?.removeEventListener('abort', abort);\n }\n}\n"],"mappings":"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","names":[]}