@brandlab-digital/site-next 3.9.0 → 3.10.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 (87) hide show
  1. package/README.md +33 -0
  2. package/dist/contact/attachments-client.d.ts +3 -0
  3. package/dist/contact/attachments-client.d.ts.map +1 -1
  4. package/dist/contact/attachments-client.js +31 -8
  5. package/dist/contact/attachments-client.js.map +1 -1
  6. package/dist/contact/client-entry.d.ts +1 -1
  7. package/dist/contact/client-entry.d.ts.map +1 -1
  8. package/dist/contact/client-entry.js.map +1 -1
  9. package/dist/contact/contract.d.ts.map +1 -1
  10. package/dist/contact/contract.js +2 -2
  11. package/dist/contact/contract.js.map +1 -1
  12. package/dist/contact/email.d.ts.map +1 -1
  13. package/dist/contact/email.js +9 -0
  14. package/dist/contact/email.js.map +1 -1
  15. package/dist/contact/form-client.d.ts +4 -2
  16. package/dist/contact/form-client.d.ts.map +1 -1
  17. package/dist/contact/form-client.js +49 -13
  18. package/dist/contact/form-client.js.map +1 -1
  19. package/dist/contact/rate-limit.d.ts +59 -0
  20. package/dist/contact/rate-limit.d.ts.map +1 -0
  21. package/dist/contact/rate-limit.js +109 -0
  22. package/dist/contact/rate-limit.js.map +1 -0
  23. package/dist/contact/route-handler.d.ts +6 -0
  24. package/dist/contact/route-handler.d.ts.map +1 -1
  25. package/dist/contact/route-handler.js +4 -0
  26. package/dist/contact/route-handler.js.map +1 -1
  27. package/dist/contact/server-entry.d.ts +1 -1
  28. package/dist/contact/server-entry.d.ts.map +1 -1
  29. package/dist/contact/server-entry.js +1 -1
  30. package/dist/contact/server-entry.js.map +1 -1
  31. package/dist/contact/server.d.ts +9 -0
  32. package/dist/contact/server.d.ts.map +1 -1
  33. package/dist/contact/server.js +71 -7
  34. package/dist/contact/server.js.map +1 -1
  35. package/dist/contact/shared.d.ts +11 -0
  36. package/dist/contact/shared.d.ts.map +1 -1
  37. package/dist/contact/shared.js +11 -0
  38. package/dist/contact/shared.js.map +1 -1
  39. package/dist/contact/testing.d.ts +5 -0
  40. package/dist/contact/testing.d.ts.map +1 -1
  41. package/dist/contact/testing.js +1 -0
  42. package/dist/contact/testing.js.map +1 -1
  43. package/dist/lead-reporting.d.ts +6 -0
  44. package/dist/lead-reporting.d.ts.map +1 -1
  45. package/dist/lead-reporting.js +13 -1
  46. package/dist/lead-reporting.js.map +1 -1
  47. package/dist/ui-kit/brandlab-skin.d.ts +12 -0
  48. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  49. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  50. package/dist/ui-kit/form/copy.d.ts +3 -0
  51. package/dist/ui-kit/form/copy.d.ts.map +1 -0
  52. package/dist/ui-kit/form/copy.js +5 -0
  53. package/dist/ui-kit/form/copy.js.map +1 -0
  54. package/dist/ui-kit/form/dialog.d.ts +24 -0
  55. package/dist/ui-kit/form/dialog.d.ts.map +1 -0
  56. package/dist/ui-kit/form/dialog.js +17 -0
  57. package/dist/ui-kit/form/dialog.js.map +1 -0
  58. package/dist/ui-kit/form/field.d.ts +30 -0
  59. package/dist/ui-kit/form/field.d.ts.map +1 -0
  60. package/dist/ui-kit/form/field.js +11 -0
  61. package/dist/ui-kit/form/field.js.map +1 -0
  62. package/dist/ui-kit/form/index.d.ts +7 -0
  63. package/dist/ui-kit/form/index.d.ts.map +1 -0
  64. package/dist/ui-kit/form/index.js +7 -0
  65. package/dist/ui-kit/form/index.js.map +1 -0
  66. package/dist/ui-kit/form/photo-upload.d.ts +37 -0
  67. package/dist/ui-kit/form/photo-upload.d.ts.map +1 -0
  68. package/dist/ui-kit/form/photo-upload.js +19 -0
  69. package/dist/ui-kit/form/photo-upload.js.map +1 -0
  70. package/dist/ui-kit/form/quote-dialog.d.ts +15 -0
  71. package/dist/ui-kit/form/quote-dialog.d.ts.map +1 -0
  72. package/dist/ui-kit/form/quote-dialog.js +68 -0
  73. package/dist/ui-kit/form/quote-dialog.js.map +1 -0
  74. package/dist/ui-kit/form/quote-form.d.ts +165 -0
  75. package/dist/ui-kit/form/quote-form.d.ts.map +1 -0
  76. package/dist/ui-kit/form/quote-form.js +235 -0
  77. package/dist/ui-kit/form/quote-form.js.map +1 -0
  78. package/dist/ui-kit/form/thank-you.d.ts +37 -0
  79. package/dist/ui-kit/form/thank-you.d.ts.map +1 -0
  80. package/dist/ui-kit/form/thank-you.js +36 -0
  81. package/dist/ui-kit/form/thank-you.js.map +1 -0
  82. package/dist/ui-kit/structure.css +197 -0
  83. package/dist/ui.d.ts +10 -1
  84. package/dist/ui.d.ts.map +1 -1
  85. package/dist/ui.js +3 -3
  86. package/dist/ui.js.map +1 -1
  87. package/package.json +6 -2
@@ -23,6 +23,7 @@ export function recordBrandlabContactLead(input) {
23
23
  formData: input.formData,
24
24
  fieldRoles: { fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) },
25
25
  ...(input.adUserDataConsent !== undefined ? { adUserDataConsent: input.adUserDataConsent } : {}),
26
+ ...(input.hasPhotos !== undefined ? { hasPhotos: input.hasPhotos } : {}),
26
27
  }, { enabled: true, gtag: (...args) => void calls.push(args) });
27
28
  return { calls, ...outcome };
28
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/contact/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAgC,MAAM,qCAAqC,CAAA;AAEzG,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAA;AAE7D,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,GAG5B,MAAM,YAAY,CAAA;AAwBnB;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqC;IAC7E,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,SAAS,CAAC,oEAAoE,CAAC,CAAA;IACpG,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IACtD,MAAM,KAAK,GAAgB,EAAE,CAAA;IAC7B,MAAM,OAAO,GAAG,kBAAkB,CAChC,IAAI,EACJ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,EACzF;QACE,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE;QAC/F,GAAG,CAAC,KAAK,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjG,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvE,CAAA;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAA;AAC9B,CAAC","sourcesContent":["import { defineContactContract, type BrandlabContactContract } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabAnalytics } from '../analytics.js'\nimport { leadBindingFor } from '../lead-registry.js'\nimport { reportAcceptedLead } from '../lead-reporting.js'\nimport { CONTACT_CONFIRMATION_ID_PATTERN } from './shared.js'\n\nexport {\n buildContactEmailSubject,\n escapeContactEmailHtml,\n renderContactEmail,\n sanitizeContactEmailSubject,\n type RenderContactEmailInput,\n type RenderedContactEmail,\n} from './email.js'\n\nexport interface RecordBrandlabContactLeadInput {\n /** The site's defineBrandlabAnalytics binding. */\n analytics: BrandlabAnalytics\n /** The site's contact contract (its email and phone roles feed Enhanced Conversions). */\n contract: BrandlabContactContract\n /** The form's pagePath, as useBrandlabContactForm receives it. */\n pagePath: string\n /** The accepted enquiry's fields, including any that onSubmitStart adds. */\n formData: FormData\n /** The provider confirmation ID (64 lowercase hex characters). */\n confirmationId: string\n adUserDataConsent?: boolean\n}\n\nexport interface RecordedBrandlabContactLead {\n /** Every gtag call, in order, that the accepted enquiry sends on the production deployment. */\n calls: unknown[][]\n ga4Sent: boolean\n enhancedConversionSent: boolean\n adsConversionSent: boolean\n}\n\n/**\n * Records exactly what useBrandlabContactForm sends for one accepted enquiry\n * on the production deployment, without a browser: the GA4 contact_form event\n * and, when the contract declares Google Ads, the form conversion. Use it to\n * pin a site's declared lead reporting in its own tests.\n */\nexport function recordBrandlabContactLead(input: RecordBrandlabContactLeadInput): RecordedBrandlabContactLead {\n const lead = leadBindingFor(input.analytics)\n if (!lead) throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.')\n if (!CONTACT_CONFIRMATION_ID_PATTERN.test(input.confirmationId)) {\n throw new TypeError('confirmationId must be 64 lowercase hexadecimal characters.')\n }\n const contract = defineContactContract(input.contract)\n const calls: unknown[][] = []\n const outcome = reportAcceptedLead(\n lead,\n { ok: true, accepted: true, trackConversion: true, confirmationId: input.confirmationId },\n {\n pagePath: input.pagePath,\n formData: input.formData,\n fieldRoles: { fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) },\n ...(input.adUserDataConsent !== undefined ? { adUserDataConsent: input.adUserDataConsent } : {}),\n },\n { enabled: true, gtag: (...args: unknown[]) => void calls.push(args) },\n )\n return { calls, ...outcome }\n}\n"]}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/contact/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAgC,MAAM,qCAAqC,CAAA;AAEzG,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAA;AAE7D,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,GAG5B,MAAM,YAAY,CAAA;AA6BnB;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqC;IAC7E,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,SAAS,CAAC,oEAAoE,CAAC,CAAA;IACpG,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IACtD,MAAM,KAAK,GAAgB,EAAE,CAAA;IAC7B,MAAM,OAAO,GAAG,kBAAkB,CAChC,IAAI,EACJ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,EACzF;QACE,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE;QAC/F,GAAG,CAAC,KAAK,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChG,GAAG,CAAC,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzE,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvE,CAAA;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAA;AAC9B,CAAC","sourcesContent":["import { defineContactContract, type BrandlabContactContract } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabAnalytics } from '../analytics.js'\nimport { leadBindingFor } from '../lead-registry.js'\nimport { reportAcceptedLead } from '../lead-reporting.js'\nimport { CONTACT_CONFIRMATION_ID_PATTERN } from './shared.js'\n\nexport {\n buildContactEmailSubject,\n escapeContactEmailHtml,\n renderContactEmail,\n sanitizeContactEmailSubject,\n type RenderContactEmailInput,\n type RenderedContactEmail,\n} from './email.js'\n\nexport interface RecordBrandlabContactLeadInput {\n /** The site's defineBrandlabAnalytics binding. */\n analytics: BrandlabAnalytics\n /** The site's contact contract (its email and phone roles feed Enhanced Conversions). */\n contract: BrandlabContactContract\n /** The form's pagePath, as useBrandlabContactForm receives it. */\n pagePath: string\n /** The accepted enquiry's fields, including any that onSubmitStart adds. */\n formData: FormData\n /** The provider confirmation ID (64 lowercase hex characters). */\n confirmationId: string\n adUserDataConsent?: boolean\n /**\n * Whether the enquiry carried photos. useBrandlabContactForm always sends contact_form's `has_photos`; pass this\n * to record it too (omitted, the recorded event has no has_photos, as before 3.10.0).\n */\n hasPhotos?: boolean\n}\n\nexport interface RecordedBrandlabContactLead {\n /** Every gtag call, in order, that the accepted enquiry sends on the production deployment. */\n calls: unknown[][]\n ga4Sent: boolean\n enhancedConversionSent: boolean\n adsConversionSent: boolean\n}\n\n/**\n * Records exactly what useBrandlabContactForm sends for one accepted enquiry\n * on the production deployment, without a browser: the GA4 contact_form event\n * and, when the contract declares Google Ads, the form conversion. Use it to\n * pin a site's declared lead reporting in its own tests.\n */\nexport function recordBrandlabContactLead(input: RecordBrandlabContactLeadInput): RecordedBrandlabContactLead {\n const lead = leadBindingFor(input.analytics)\n if (!lead) throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.')\n if (!CONTACT_CONFIRMATION_ID_PATTERN.test(input.confirmationId)) {\n throw new TypeError('confirmationId must be 64 lowercase hexadecimal characters.')\n }\n const contract = defineContactContract(input.contract)\n const calls: unknown[][] = []\n const outcome = reportAcceptedLead(\n lead,\n { ok: true, accepted: true, trackConversion: true, confirmationId: input.confirmationId },\n {\n pagePath: input.pagePath,\n formData: input.formData,\n fieldRoles: { fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) },\n ...(input.adUserDataConsent !== undefined ? { adUserDataConsent: input.adUserDataConsent } : {}),\n ...(input.hasPhotos !== undefined ? { hasPhotos: input.hasPhotos } : {}),\n },\n { enabled: true, gtag: (...args: unknown[]) => void calls.push(args) },\n )\n return { calls, ...outcome }\n}\n"]}
@@ -5,6 +5,8 @@ import { type LeadAdsParams, type LeadBinding, type LeadPageParams, type LeadTra
5
5
  export type AcceptedContactResult = Extract<ContactActionResult, {
6
6
  ok: true;
7
7
  }>;
8
+ /** True when the attempt carried at least one photo: a non-empty image file entry (a PDF is not a photo). */
9
+ export declare function contactFormDataHasPhotos(formData: FormData): boolean;
8
10
  export declare function isTrackableContactActionResult(result: ContactActionResult): result is Extract<ContactActionResult, {
9
11
  ok: true;
10
12
  accepted: true;
@@ -26,6 +28,8 @@ export declare function trackDeliveredContactWithEnhancedConversions(result: Con
26
28
  adUserDataConsent?: boolean;
27
29
  params?: LeadPageParams;
28
30
  adsParams?: LeadAdsParams;
31
+ /** Sent as contact_form's non-personal `has_photos` ("true" or "false") when given. */
32
+ hasPhotos?: boolean;
29
33
  }): ContactFormConversionResult;
30
34
  export interface AcceptedLeadInput {
31
35
  /** The form's pagePath, exactly as the hook received it. */
@@ -35,6 +39,8 @@ export interface AcceptedLeadInput {
35
39
  fieldRoles: BrandlabContactFieldRoles;
36
40
  /** The site's live ad_user_data decision, when it has one (see trackDeliveredContactWithEnhancedConversions). */
37
41
  adUserDataConsent?: boolean;
42
+ /** Whether the enquiry carried photos, for contact_form's `has_photos`; omitted, the event has no has_photos. */
43
+ hasPhotos?: boolean;
38
44
  }
39
45
  /** Sends the binding's declared lead for one accepted enquiry. */
40
46
  export declare function reportAcceptedLead(lead: LeadBinding, result: AcceptedContactResult, input: AcceptedLeadInput, override?: Pick<LeadTrackingConfig, 'enabled' | 'gtag'>): ContactFormConversionResult;
@@ -1 +1 @@
1
- {"version":3,"file":"lead-reporting.d.ts","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,2BAA2B,EACjC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AAC9E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAAA;AA2B9E,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,mBAAmB,GAC1B,MAAM,IAAI,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC,CAEtE;AAaD;;;;GAIG;AACH,wBAAgB,4CAA4C,CAC1D,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAA;IAChB,+EAA+E;IAC/E,UAAU,EAAE,yBAAyB,CAAA;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,SAAS,CAAC,EAAE,aAAa,CAAA;CAC1B,GACA,2BAA2B,CAwB7B;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB,UAAU,EAAE,yBAAyB,CAAA;IACrC,iHAAiH;IACjH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,iBAAiB,EACxB,QAAQ,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,MAAM,CAAC,GACtD,2BAA2B,CAU7B"}
1
+ {"version":3,"file":"lead-reporting.d.ts","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,2BAA2B,EACjC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AAC9E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAAA;AA2B9E,6GAA6G;AAC7G,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,WAK1D;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,mBAAmB,GAC1B,MAAM,IAAI,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC,CAEtE;AAaD;;;;GAIG;AACH,wBAAgB,4CAA4C,CAC1D,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAA;IAChB,+EAA+E;IAC/E,UAAU,EAAE,yBAAyB,CAAA;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,uFAAuF;IACvF,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,GACA,2BAA2B,CA2B7B;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB,UAAU,EAAE,yBAAyB,CAAA;IACrC,iHAAiH;IACjH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,iHAAiH;IACjH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,iBAAiB,EACxB,QAAQ,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,MAAM,CAAC,GACtD,2BAA2B,CAW7B"}
@@ -26,6 +26,14 @@ function sanitizeAdsParams(params) {
26
26
  : {}),
27
27
  };
28
28
  }
29
+ /** True when the attempt carried at least one photo: a non-empty image file entry (a PDF is not a photo). */
30
+ export function contactFormDataHasPhotos(formData) {
31
+ for (const value of formData.values()) {
32
+ if (typeof value !== 'string' && value.size > 0 && value.type.toLowerCase().startsWith('image/'))
33
+ return true;
34
+ }
35
+ return false;
36
+ }
29
37
  export function isTrackableContactActionResult(result) {
30
38
  return result.ok && result.accepted && result.trackConversion;
31
39
  }
@@ -52,7 +60,10 @@ export function trackDeliveredContactWithEnhancedConversions(result, config, opt
52
60
  gtag: config.gtag ?? getBrowserGtag(),
53
61
  pagePath: safePagePath(options.pagePath),
54
62
  submissionId: result.confirmationId,
55
- params: sanitizeContactParams(options.params),
63
+ params: {
64
+ ...sanitizeContactParams(options.params),
65
+ ...(options.hasPhotos !== undefined ? { has_photos: options.hasPhotos ? 'true' : 'false' } : {}),
66
+ },
56
67
  adsParams: sanitizeAdsParams(options.adsParams),
57
68
  enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,
58
69
  // Absent: the contract's default. Anything given counts only as an explicit true (fail closed).
@@ -72,6 +83,7 @@ export function reportAcceptedLead(lead, result, input, override) {
72
83
  ...(input.adUserDataConsent !== undefined ? { adUserDataConsent: input.adUserDataConsent } : {}),
73
84
  params,
74
85
  adsParams: lead.adsParams({ pagePath: input.pagePath, pageParams: params, formData: input.formData }),
86
+ ...(input.hasPhotos !== undefined ? { hasPhotos: input.hasPhotos } : {}),
75
87
  });
76
88
  }
77
89
  //# sourceMappingURL=lead-reporting.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lead-reporting.js","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"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","sourcesContent":["// Internal module (not in package exports): the single accepted-enquiry\n// reporting path, used only by useBrandlabContactForm and the contact testing\n// surface, with the lead reporting a binding declared in lead-registry.\nimport {\n normalizeEnhancedConversionEmail,\n normalizeEnhancedConversionPhone,\n sendContactFormConversion,\n type ContactFormConversionResult,\n} from '@brandlab-digital/site-core/analytics'\nimport type { ContactActionResult } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabContactFieldRoles } from './contact/shared.js'\nimport {\n getBrowserGtag,\n safePagePath,\n safeSlug,\n type LeadAdsParams,\n type LeadBinding,\n type LeadPageParams,\n type LeadTrackingConfig,\n} from './lead-registry.js'\n\nexport type AcceptedContactResult = Extract<ContactActionResult, { ok: true }>\n\nfunction sanitizeContactParams(params: LeadPageParams | undefined) {\n return {\n ...(safeSlug(params?.page_template)\n ? { page_template: safeSlug(params?.page_template) }\n : {}),\n ...(safeSlug(params?.service_slug)\n ? { service_slug: safeSlug(params?.service_slug) }\n : {}),\n ...(safeSlug(params?.location_slug)\n ? { location_slug: safeSlug(params?.location_slug) }\n : {}),\n }\n}\n\nfunction sanitizeAdsParams(params: LeadAdsParams | undefined) {\n return {\n ...(params?.currency && /^[A-Z]{3}$/.test(params.currency)\n ? { currency: params.currency }\n : {}),\n ...(typeof params?.value === 'number' && Number.isFinite(params.value) && params.value >= 0\n ? { value: params.value }\n : {}),\n }\n}\n\nexport function isTrackableContactActionResult(\n result: ContactActionResult,\n): result is Extract<ContactActionResult, { ok: true; accepted: true }> {\n return result.ok && result.accepted && result.trackConversion\n}\n\nfunction explicitRoleValue(\n definition: BrandlabContactFieldRoles,\n formData: FormData,\n role: 'email' | 'phone',\n) {\n const field = definition.fields.find((candidate) => candidate.role === role)\n if (!field) return undefined\n const value = formData.get(field.name)\n return typeof value === 'string' ? value : undefined\n}\n\n/**\n * GA4 contact_form plus the Google Ads form conversion, only after provider\n * acceptance. Email and phone may only come from fields with explicit\n * declarative roles and never enter the GA4 parameter object.\n */\nexport function trackDeliveredContactWithEnhancedConversions(\n result: ContactActionResult,\n config: LeadTrackingConfig,\n options: {\n pagePath: string\n /** Field names with declarative roles; only email and phone roles are read. */\n definition: BrandlabContactFieldRoles\n formData: FormData\n /**\n * Live Consent Mode decision for ad_user_data. Absent: deny, or grant for a\n * disclosed contract. An explicit `false` is always deny.\n */\n adUserDataConsent?: boolean\n params?: LeadPageParams\n adsParams?: LeadAdsParams\n },\n): ContactFormConversionResult {\n if (!isTrackableContactActionResult(result)) {\n return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false }\n }\n const email = normalizeEnhancedConversionEmail(\n explicitRoleValue(options.definition, options.formData, 'email'),\n )\n const phone = normalizeEnhancedConversionPhone(\n explicitRoleValue(options.definition, options.formData, 'phone'),\n )\n return sendContactFormConversion({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n pagePath: safePagePath(options.pagePath),\n submissionId: result.confirmationId,\n params: sanitizeContactParams(options.params),\n adsParams: sanitizeAdsParams(options.adsParams),\n enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,\n // Absent: the contract's default. Anything given counts only as an explicit true (fail closed).\n adUserDataConsent: options.adUserDataConsent === undefined\n ? config.adUserDataGrantedByDefault === true\n : options.adUserDataConsent === true,\n ...((email || phone) ? { enhancedConversion: { email, phone } } : {}),\n })\n}\n\nexport interface AcceptedLeadInput {\n /** The form's pagePath, exactly as the hook received it. */\n pagePath: string\n /** The accepted attempt's fields. */\n formData: FormData\n fieldRoles: BrandlabContactFieldRoles\n /** The site's live ad_user_data decision, when it has one (see trackDeliveredContactWithEnhancedConversions). */\n adUserDataConsent?: boolean\n}\n\n/** Sends the binding's declared lead for one accepted enquiry. */\nexport function reportAcceptedLead(\n lead: LeadBinding,\n result: AcceptedContactResult,\n input: AcceptedLeadInput,\n override?: Pick<LeadTrackingConfig, 'enabled' | 'gtag'>,\n): ContactFormConversionResult {\n const params = lead.pageParams(input.pagePath)\n return trackDeliveredContactWithEnhancedConversions(result, { ...lead.config, ...override }, {\n pagePath: input.pagePath,\n definition: input.fieldRoles,\n formData: input.formData,\n ...(input.adUserDataConsent !== undefined ? { adUserDataConsent: input.adUserDataConsent } : {}),\n params,\n adsParams: lead.adsParams({ pagePath: input.pagePath, pageParams: params, formData: input.formData }),\n })\n}\n"]}
1
+ {"version":3,"file":"lead-reporting.js","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"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","sourcesContent":["// Internal module (not in package exports): the single accepted-enquiry\n// reporting path, used only by useBrandlabContactForm and the contact testing\n// surface, with the lead reporting a binding declared in lead-registry.\nimport {\n normalizeEnhancedConversionEmail,\n normalizeEnhancedConversionPhone,\n sendContactFormConversion,\n type ContactFormConversionResult,\n} from '@brandlab-digital/site-core/analytics'\nimport type { ContactActionResult } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabContactFieldRoles } from './contact/shared.js'\nimport {\n getBrowserGtag,\n safePagePath,\n safeSlug,\n type LeadAdsParams,\n type LeadBinding,\n type LeadPageParams,\n type LeadTrackingConfig,\n} from './lead-registry.js'\n\nexport type AcceptedContactResult = Extract<ContactActionResult, { ok: true }>\n\nfunction sanitizeContactParams(params: LeadPageParams | undefined) {\n return {\n ...(safeSlug(params?.page_template)\n ? { page_template: safeSlug(params?.page_template) }\n : {}),\n ...(safeSlug(params?.service_slug)\n ? { service_slug: safeSlug(params?.service_slug) }\n : {}),\n ...(safeSlug(params?.location_slug)\n ? { location_slug: safeSlug(params?.location_slug) }\n : {}),\n }\n}\n\nfunction sanitizeAdsParams(params: LeadAdsParams | undefined) {\n return {\n ...(params?.currency && /^[A-Z]{3}$/.test(params.currency)\n ? { currency: params.currency }\n : {}),\n ...(typeof params?.value === 'number' && Number.isFinite(params.value) && params.value >= 0\n ? { value: params.value }\n : {}),\n }\n}\n\n/** True when the attempt carried at least one photo: a non-empty image file entry (a PDF is not a photo). */\nexport function contactFormDataHasPhotos(formData: FormData) {\n for (const value of formData.values()) {\n if (typeof value !== 'string' && value.size > 0 && value.type.toLowerCase().startsWith('image/')) return true\n }\n return false\n}\n\nexport function isTrackableContactActionResult(\n result: ContactActionResult,\n): result is Extract<ContactActionResult, { ok: true; accepted: true }> {\n return result.ok && result.accepted && result.trackConversion\n}\n\nfunction explicitRoleValue(\n definition: BrandlabContactFieldRoles,\n formData: FormData,\n role: 'email' | 'phone',\n) {\n const field = definition.fields.find((candidate) => candidate.role === role)\n if (!field) return undefined\n const value = formData.get(field.name)\n return typeof value === 'string' ? value : undefined\n}\n\n/**\n * GA4 contact_form plus the Google Ads form conversion, only after provider\n * acceptance. Email and phone may only come from fields with explicit\n * declarative roles and never enter the GA4 parameter object.\n */\nexport function trackDeliveredContactWithEnhancedConversions(\n result: ContactActionResult,\n config: LeadTrackingConfig,\n options: {\n pagePath: string\n /** Field names with declarative roles; only email and phone roles are read. */\n definition: BrandlabContactFieldRoles\n formData: FormData\n /**\n * Live Consent Mode decision for ad_user_data. Absent: deny, or grant for a\n * disclosed contract. An explicit `false` is always deny.\n */\n adUserDataConsent?: boolean\n params?: LeadPageParams\n adsParams?: LeadAdsParams\n /** Sent as contact_form's non-personal `has_photos` (\"true\" or \"false\") when given. */\n hasPhotos?: boolean\n },\n): ContactFormConversionResult {\n if (!isTrackableContactActionResult(result)) {\n return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false }\n }\n const email = normalizeEnhancedConversionEmail(\n explicitRoleValue(options.definition, options.formData, 'email'),\n )\n const phone = normalizeEnhancedConversionPhone(\n explicitRoleValue(options.definition, options.formData, 'phone'),\n )\n return sendContactFormConversion({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n pagePath: safePagePath(options.pagePath),\n submissionId: result.confirmationId,\n params: {\n ...sanitizeContactParams(options.params),\n ...(options.hasPhotos !== undefined ? { has_photos: options.hasPhotos ? 'true' : 'false' } : {}),\n },\n adsParams: sanitizeAdsParams(options.adsParams),\n enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,\n // Absent: the contract's default. Anything given counts only as an explicit true (fail closed).\n adUserDataConsent: options.adUserDataConsent === undefined\n ? config.adUserDataGrantedByDefault === true\n : options.adUserDataConsent === true,\n ...((email || phone) ? { enhancedConversion: { email, phone } } : {}),\n })\n}\n\nexport interface AcceptedLeadInput {\n /** The form's pagePath, exactly as the hook received it. */\n pagePath: string\n /** The accepted attempt's fields. */\n formData: FormData\n fieldRoles: BrandlabContactFieldRoles\n /** The site's live ad_user_data decision, when it has one (see trackDeliveredContactWithEnhancedConversions). */\n adUserDataConsent?: boolean\n /** Whether the enquiry carried photos, for contact_form's `has_photos`; omitted, the event has no has_photos. */\n hasPhotos?: boolean\n}\n\n/** Sends the binding's declared lead for one accepted enquiry. */\nexport function reportAcceptedLead(\n lead: LeadBinding,\n result: AcceptedContactResult,\n input: AcceptedLeadInput,\n override?: Pick<LeadTrackingConfig, 'enabled' | 'gtag'>,\n): ContactFormConversionResult {\n const params = lead.pageParams(input.pagePath)\n return trackDeliveredContactWithEnhancedConversions(result, { ...lead.config, ...override }, {\n pagePath: input.pagePath,\n definition: input.fieldRoles,\n formData: input.formData,\n ...(input.adUserDataConsent !== undefined ? { adUserDataConsent: input.adUserDataConsent } : {}),\n params,\n adsParams: lead.adsParams({ pagePath: input.pagePath, pageParams: params, formData: input.formData }),\n ...(input.hasPhotos !== undefined ? { hasPhotos: input.hasPhotos } : {}),\n })\n}\n"]}
@@ -11,6 +11,12 @@ import type { MobileCallBarSkin } from './shell/mobile-call-bar.js';
11
11
  import type { MobileDrawerSkin } from './shell/mobile-drawer.js';
12
12
  import type { SiteHeaderSkin } from './shell/site-header.js';
13
13
  import type { UtilityBarSkin } from './shell/utility-bar.js';
14
+ import type { DialogSkin } from './form/dialog.js';
15
+ import type { FieldSkin } from './form/field.js';
16
+ import type { PhotoUploadSkin } from './form/photo-upload.js';
17
+ import type { QuoteDialogSkin } from './form/quote-dialog.js';
18
+ import type { QuoteFormSkin } from './form/quote-form.js';
19
+ import type { ThankYouSkin } from './form/thank-you.js';
14
20
  export interface BrandlabSkin {
15
21
  readonly buttonLink?: ButtonLinkSkin;
16
22
  readonly textLink?: TextLinkSkin;
@@ -23,5 +29,11 @@ export interface BrandlabSkin {
23
29
  readonly utilityBar?: UtilityBarSkin;
24
30
  readonly mobileCallBar?: MobileCallBarSkin;
25
31
  readonly backToTop?: BackToTopSkin;
32
+ readonly quoteForm?: QuoteFormSkin;
33
+ readonly quoteDialog?: QuoteDialogSkin;
34
+ readonly photoUpload?: PhotoUploadSkin;
35
+ readonly dialog?: DialogSkin;
36
+ readonly field?: FieldSkin;
37
+ readonly thankYou?: ThankYouSkin;
26
38
  }
27
39
  //# sourceMappingURL=brandlab-skin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"brandlab-skin.d.ts","sourceRoot":"","sources":["../../src/ui-kit/brandlab-skin.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAE5D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAA;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,gBAAgB,CAAA;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAA;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;CACnC"}
1
+ {"version":3,"file":"brandlab-skin.d.ts","sourceRoot":"","sources":["../../src/ui-kit/brandlab-skin.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAA;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,gBAAgB,CAAA;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAA;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAElC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAA;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;CACjC"}
@@ -1 +1 @@
1
- {"version":3,"file":"brandlab-skin.js","sourceRoot":"","sources":["../../src/ui-kit/brandlab-skin.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * A site's whole look for the ui-kit: one entry per shared component it renders (`src/site/skin.ts`, written with\n * `satisfies BrandlabSkin`). Each release adds the entries for its components. K2 checks that every rendered\n * component has an entry and that no reference-skin class ships.\n */\nimport type { ButtonLinkSkin, PhoneLinkSkin, TextLinkSkin } from './primitives/links.js'\nimport type { QuoteButtonSkin } from './primitives/quote-button.js'\nimport type { ContainerSkin, SectionHeadingSkin } from './primitives/layout.js'\nimport type { BackToTopSkin } from './shell/back-to-top.js'\nimport type { MobileCallBarSkin } from './shell/mobile-call-bar.js'\nimport type { MobileDrawerSkin } from './shell/mobile-drawer.js'\nimport type { SiteHeaderSkin } from './shell/site-header.js'\nimport type { UtilityBarSkin } from './shell/utility-bar.js'\n\nexport interface BrandlabSkin {\n readonly buttonLink?: ButtonLinkSkin\n readonly textLink?: TextLinkSkin\n readonly phoneLink?: PhoneLinkSkin\n readonly quoteButton?: QuoteButtonSkin\n readonly container?: ContainerSkin\n readonly sectionHeading?: SectionHeadingSkin\n readonly siteHeader?: SiteHeaderSkin\n readonly mobileDrawer?: MobileDrawerSkin\n readonly utilityBar?: UtilityBarSkin\n readonly mobileCallBar?: MobileCallBarSkin\n readonly backToTop?: BackToTopSkin\n}\n"]}
1
+ {"version":3,"file":"brandlab-skin.js","sourceRoot":"","sources":["../../src/ui-kit/brandlab-skin.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * A site's whole look for the ui-kit: one entry per shared component it renders (`src/site/skin.ts`, written with\n * `satisfies BrandlabSkin`). Each release adds the entries for its components. K2 checks that every rendered\n * component has an entry and that no reference-skin class ships.\n */\nimport type { ButtonLinkSkin, PhoneLinkSkin, TextLinkSkin } from './primitives/links.js'\nimport type { QuoteButtonSkin } from './primitives/quote-button.js'\nimport type { ContainerSkin, SectionHeadingSkin } from './primitives/layout.js'\nimport type { BackToTopSkin } from './shell/back-to-top.js'\nimport type { MobileCallBarSkin } from './shell/mobile-call-bar.js'\nimport type { MobileDrawerSkin } from './shell/mobile-drawer.js'\nimport type { SiteHeaderSkin } from './shell/site-header.js'\nimport type { UtilityBarSkin } from './shell/utility-bar.js'\nimport type { DialogSkin } from './form/dialog.js'\nimport type { FieldSkin } from './form/field.js'\nimport type { PhotoUploadSkin } from './form/photo-upload.js'\nimport type { QuoteDialogSkin } from './form/quote-dialog.js'\nimport type { QuoteFormSkin } from './form/quote-form.js'\nimport type { ThankYouSkin } from './form/thank-you.js'\n\nexport interface BrandlabSkin {\n readonly buttonLink?: ButtonLinkSkin\n readonly textLink?: TextLinkSkin\n readonly phoneLink?: PhoneLinkSkin\n readonly quoteButton?: QuoteButtonSkin\n readonly container?: ContainerSkin\n readonly sectionHeading?: SectionHeadingSkin\n readonly siteHeader?: SiteHeaderSkin\n readonly mobileDrawer?: MobileDrawerSkin\n readonly utilityBar?: UtilityBarSkin\n readonly mobileCallBar?: MobileCallBarSkin\n readonly backToTop?: BackToTopSkin\n // form (3.10.0)\n readonly quoteForm?: QuoteFormSkin\n readonly quoteDialog?: QuoteDialogSkin\n readonly photoUpload?: PhotoUploadSkin\n readonly dialog?: DialogSkin\n readonly field?: FieldSkin\n readonly thankYou?: ThankYouSkin\n}\n"]}
@@ -0,0 +1,3 @@
1
+ /** Fills `{name}` tokens in a site's copy template, for example `"{count} of {max}"`. Unknown tokens stay visible. */
2
+ export declare function fillCopy(template: string, values: Readonly<Record<string, string | number>>): string;
3
+ //# sourceMappingURL=copy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/copy.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GAAG,MAAM,CAEpG"}
@@ -0,0 +1,5 @@
1
+ /** Fills `{name}` tokens in a site's copy template, for example `"{count} of {max}"`. Unknown tokens stay visible. */
2
+ export function fillCopy(template, values) {
3
+ return template.replace(/\{([a-zA-Z]+)\}/g, (token, key) => (key in values ? String(values[key]) : token));
4
+ }
5
+ //# sourceMappingURL=copy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy.js","sourceRoot":"","sources":["../../../src/ui-kit/form/copy.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,MAAiD;IAC1F,OAAO,QAAQ,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;AACpH,CAAC","sourcesContent":["/** Fills `{name}` tokens in a site's copy template, for example `\"{count} of {max}\"`. Unknown tokens stay visible. */\nexport function fillCopy(template: string, values: Readonly<Record<string, string | number>>): string {\n return template.replace(/\\{([a-zA-Z]+)\\}/g, (token, key: string) => (key in values ? String(values[key]) : token))\n}\n"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `Dialog`: a modal dialog over `HeadlessDialog` (native `<dialog>` with `showModal()`, so the page behind is inert).
3
+ * Its H2 title names it and takes focus when it opens, so a phone keyboard doesn't jump up; Escape and the close
4
+ * button close it and focus returns to the control that opened it. The page doesn't scroll behind it (structure.css).
5
+ */
6
+ import { type ReactNode, type RefObject } from 'react';
7
+ import { type SkinEntry } from '../skin.js';
8
+ export type DialogSlot = 'root' | 'header' | 'title' | 'close' | 'body';
9
+ export type DialogSkin = SkinEntry<DialogSlot>;
10
+ export interface DialogProps extends DialogSkin {
11
+ /** The dialog's id; its title is `${id}-title`. */
12
+ readonly id: string;
13
+ readonly open: boolean;
14
+ readonly onClose: () => void;
15
+ /** The H2 that names the dialog, for example "Get a free quote". */
16
+ readonly title: string;
17
+ /** The close button's accessible name, for example "Close". */
18
+ readonly closeLabel: string;
19
+ /** Where focus returns on close; by default the element focused when it opened. */
20
+ readonly returnFocusRef?: RefObject<HTMLElement | null> | undefined;
21
+ readonly children: ReactNode;
22
+ }
23
+ export declare function Dialog({ id, open, onClose, title, closeLabel, returnFocusRef, classes, children }: DialogProps): import("react/jsx-runtime").JSX.Element;
24
+ //# sourceMappingURL=dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/dialog.tsx"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,OAAO,EAAU,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG9D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AACvE,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;AAE9C,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,mDAAmD;IACnD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,SAAS,CAAA;IACnE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,WAAW,2CA2B9G"}
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * `Dialog`: a modal dialog over `HeadlessDialog` (native `<dialog>` with `showModal()`, so the page behind is inert).
5
+ * Its H2 title names it and takes focus when it opens, so a phone keyboard doesn't jump up; Escape and the close
6
+ * button close it and focus returns to the control that opened it. The page doesn't scroll behind it (structure.css).
7
+ */
8
+ import { useRef } from 'react';
9
+ import { HeadlessDialog } from '../../ui-client.js';
10
+ import { Icon } from '../icons.js';
11
+ import { slot } from '../skin.js';
12
+ export function Dialog({ id, open, onClose, title, closeLabel, returnFocusRef, classes, children }) {
13
+ const titleRef = useRef(null);
14
+ const titleId = `${id}-title`;
15
+ return (_jsxs(HeadlessDialog, { id: id, open: open, onClose: onClose, labelledBy: titleId, initialFocusRef: titleRef, ...(returnFocusRef ? { returnFocusRef } : {}), "data-bl": "dialog", "data-state": open ? 'open' : 'closed', className: classes?.root, children: [_jsxs("div", { ...slot(classes, 'header'), children: [_jsx("h2", { ...slot(classes, 'title'), id: titleId, ref: titleRef, tabIndex: -1, children: title }), _jsxs("button", { ...slot(classes, 'close'), type: "button", onClick: onClose, children: [_jsx(Icon, { name: "close" }), _jsx("span", { "data-bl-visually-hidden": "", children: closeLabel })] })] }), _jsx("div", { ...slot(classes, 'body'), children: children })] }));
16
+ }
17
+ //# sourceMappingURL=dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../../src/ui-kit/form/dialog.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;GAIG;AACH,OAAO,EAAE,MAAM,EAAkC,MAAM,OAAO,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAmBjD,MAAM,UAAU,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAe;IAC7G,MAAM,QAAQ,GAAG,MAAM,CAAqB,IAAI,CAAC,CAAA;IACjD,MAAM,OAAO,GAAG,GAAG,EAAE,QAAQ,CAAA;IAC7B,OAAO,CACL,MAAC,cAAc,IACb,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,OAAO,EACnB,eAAe,EAAE,QAAQ,KACrB,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aACtC,QAAQ,gBACJ,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EACpC,SAAS,EAAE,OAAO,EAAE,IAAI,aAExB,kBAAS,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,aAC9B,gBAAQ,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,YACrE,KAAK,GACH,EACL,qBAAY,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,OAAO,aAChE,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,GAAG,EACrB,0CAA8B,EAAE,YAAE,UAAU,GAAQ,IAC7C,IACL,EACN,iBAAS,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAAG,QAAQ,GAAO,IACjC,CAClB,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/**\n * `Dialog`: a modal dialog over `HeadlessDialog` (native `<dialog>` with `showModal()`, so the page behind is inert).\n * Its H2 title names it and takes focus when it opens, so a phone keyboard doesn't jump up; Escape and the close\n * button close it and focus returns to the control that opened it. The page doesn't scroll behind it (structure.css).\n */\nimport { useRef, type ReactNode, type RefObject } from 'react'\nimport { HeadlessDialog } from '../../ui-client.js'\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport type DialogSlot = 'root' | 'header' | 'title' | 'close' | 'body'\nexport type DialogSkin = SkinEntry<DialogSlot>\n\nexport interface DialogProps extends DialogSkin {\n /** The dialog's id; its title is `${id}-title`. */\n readonly id: string\n readonly open: boolean\n readonly onClose: () => void\n /** The H2 that names the dialog, for example \"Get a free quote\". */\n readonly title: string\n /** The close button's accessible name, for example \"Close\". */\n readonly closeLabel: string\n /** Where focus returns on close; by default the element focused when it opened. */\n readonly returnFocusRef?: RefObject<HTMLElement | null> | undefined\n readonly children: ReactNode\n}\n\nexport function Dialog({ id, open, onClose, title, closeLabel, returnFocusRef, classes, children }: DialogProps) {\n const titleRef = useRef<HTMLHeadingElement>(null)\n const titleId = `${id}-title`\n return (\n <HeadlessDialog\n id={id}\n open={open}\n onClose={onClose}\n labelledBy={titleId}\n initialFocusRef={titleRef}\n {...(returnFocusRef ? { returnFocusRef } : {})}\n data-bl=\"dialog\"\n data-state={open ? 'open' : 'closed'}\n className={classes?.root}\n >\n <div {...slot(classes, 'header')}>\n <h2 {...slot(classes, 'title')} id={titleId} ref={titleRef} tabIndex={-1}>\n {title}\n </h2>\n <button {...slot(classes, 'close')} type=\"button\" onClick={onClose}>\n <Icon name=\"close\" />\n <span data-bl-visually-hidden=\"\">{closeLabel}</span>\n </button>\n </div>\n <div {...slot(classes, 'body')}>{children}</div>\n </HeadlessDialog>\n )\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `Field`: one labelled control over `HeadlessField`. The label sits above the control; a description and an error
3
+ * are wired to it with `aria-describedby`, the error is `role="alert"` and the control gets `aria-invalid` while it
4
+ * has one. A required control carries `required` and `aria-required`, never a visible "(required)". Server-safe: it
5
+ * holds no state, so it works in server and client components alike.
6
+ */
7
+ import type { ReactNode } from 'react';
8
+ import type { SkinEntry } from '../skin.js';
9
+ export type FieldSlot = 'field' | 'label' | 'description' | 'error';
10
+ export type FieldSkin = SkinEntry<FieldSlot>;
11
+ /** Spread onto the control (`input`, `select`, `textarea` or a file input). */
12
+ export interface FieldControlProps {
13
+ readonly id: string;
14
+ readonly required: boolean;
15
+ readonly 'aria-required': true | undefined;
16
+ readonly 'aria-invalid': true | undefined;
17
+ readonly 'aria-describedby': string | undefined;
18
+ }
19
+ export interface FieldProps extends FieldSkin {
20
+ /** The control's id; the description and error ids are `${id}-description` and `${id}-error`. */
21
+ readonly id: string;
22
+ readonly label: string;
23
+ readonly description?: string | undefined;
24
+ /** The error message, shown only once there is one (after a submit attempt). */
25
+ readonly error?: string | undefined;
26
+ readonly required?: boolean;
27
+ readonly children: (control: FieldControlProps) => ReactNode;
28
+ }
29
+ export declare function Field({ id, label, description, error, required, classes, children }: FieldProps): import("react/jsx-runtime").JSX.Element;
30
+ //# sourceMappingURL=field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/field.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,aAAa,GAAG,OAAO,CAAA;AACnE,MAAM,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,CAAA;AAE5C,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,eAAe,EAAE,IAAI,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,cAAc,EAAE,IAAI,GAAG,SAAS,CAAA;IACzC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAA;CAChD;AAED,MAAM,WAAW,UAAW,SAAQ,SAAS;IAC3C,iGAAiG;IACjG,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACzC,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,SAAS,CAAA;CAC7D;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,QAAgB,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,UAAU,2CAsBvG"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { HeadlessField } from '../../ui.js';
3
+ export function Field({ id, label, description, error, required = false, classes, children }) {
4
+ return (_jsx(HeadlessField, { id: id, label: label, description: description, error: error, required: required, className: classes?.field, labelClassName: classes?.label, descriptionClassName: classes?.description, errorClassName: classes?.error, dataAttributes: {
5
+ field: { 'data-bl': 'field', 'data-slot': 'field' },
6
+ label: { 'data-slot': 'label' },
7
+ description: { 'data-slot': 'description' },
8
+ error: { 'data-slot': 'error' },
9
+ }, children: (control) => children({ ...control, 'aria-required': required ? true : undefined }) }));
10
+ }
11
+ //# sourceMappingURL=field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field.js","sourceRoot":"","sources":["../../../src/ui-kit/form/field.tsx"],"names":[],"mappings":";AAOA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AA0B3C,MAAM,UAAU,KAAK,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,GAAG,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAc;IACtG,OAAO,CACL,KAAC,aAAa,IACZ,EAAE,EAAE,EAAE,EACN,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,OAAO,EAAE,KAAK,EACzB,cAAc,EAAE,OAAO,EAAE,KAAK,EAC9B,oBAAoB,EAAE,OAAO,EAAE,WAAW,EAC1C,cAAc,EAAE,OAAO,EAAE,KAAK,EAC9B,cAAc,EAAE;YACd,KAAK,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE;YACnD,KAAK,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE;YAC/B,WAAW,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE;YAC3C,KAAK,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE;SAChC,YAEA,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,GACtE,CACjB,CAAA;AACH,CAAC","sourcesContent":["/**\n * `Field`: one labelled control over `HeadlessField`. The label sits above the control; a description and an error\n * are wired to it with `aria-describedby`, the error is `role=\"alert\"` and the control gets `aria-invalid` while it\n * has one. A required control carries `required` and `aria-required`, never a visible \"(required)\". Server-safe: it\n * holds no state, so it works in server and client components alike.\n */\nimport type { ReactNode } from 'react'\nimport { HeadlessField } from '../../ui.js'\nimport type { SkinEntry } from '../skin.js'\n\nexport type FieldSlot = 'field' | 'label' | 'description' | 'error'\nexport type FieldSkin = SkinEntry<FieldSlot>\n\n/** Spread onto the control (`input`, `select`, `textarea` or a file input). */\nexport interface FieldControlProps {\n readonly id: string\n readonly required: boolean\n readonly 'aria-required': true | undefined\n readonly 'aria-invalid': true | undefined\n readonly 'aria-describedby': string | undefined\n}\n\nexport interface FieldProps extends FieldSkin {\n /** The control's id; the description and error ids are `${id}-description` and `${id}-error`. */\n readonly id: string\n readonly label: string\n readonly description?: string | undefined\n /** The error message, shown only once there is one (after a submit attempt). */\n readonly error?: string | undefined\n readonly required?: boolean\n readonly children: (control: FieldControlProps) => ReactNode\n}\n\nexport function Field({ id, label, description, error, required = false, classes, children }: FieldProps) {\n return (\n <HeadlessField\n id={id}\n label={label}\n description={description}\n error={error}\n required={required}\n className={classes?.field}\n labelClassName={classes?.label}\n descriptionClassName={classes?.description}\n errorClassName={classes?.error}\n dataAttributes={{\n field: { 'data-bl': 'field', 'data-slot': 'field' },\n label: { 'data-slot': 'label' },\n description: { 'data-slot': 'description' },\n error: { 'data-slot': 'error' },\n }}\n >\n {(control) => children({ ...control, 'aria-required': required ? true : undefined })}\n </HeadlessField>\n )\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export { QUOTE_FORM_PLACEMENTS, QUOTE_FORM_PLACEMENT_VARIANTS, QUOTE_FORM_VARIANTS, QuoteForm, createQuoteForm, type BoundQuoteDialogProps, type BoundQuoteForm, type BoundQuoteFormProps, type CreateQuoteFormOptions, type QuoteFormCopy, type QuoteFormLayout, type QuoteFormPlacement, type QuoteFormProps, type QuoteFormSkin, type QuoteFormSlot, type QuoteFormVariant, } from './quote-form.js';
2
+ export { QuoteDialog, type QuoteDialogProps, type QuoteDialogSkin } from './quote-dialog.js';
3
+ export { PhotoUpload, type PhotoUploadProps, type PhotoUploadSkin, type PhotoUploadSlot } from './photo-upload.js';
4
+ export { Dialog, type DialogProps, type DialogSkin, type DialogSlot } from './dialog.js';
5
+ export { Field, type FieldControlProps, type FieldProps, type FieldSkin, type FieldSlot } from './field.js';
6
+ export { THANK_YOU_DEFAULT_REPLY_TIME, ThankYou, thankYouMetadata, type ThankYouProps, type ThankYouSkin, type ThankYouSlot, } from './thank-you.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,6BAA6B,EAC7B,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAC5F,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAClH,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AACxF,OAAO,EAAE,KAAK,EAAE,KAAK,iBAAiB,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3G,OAAO,EACL,4BAA4B,EAC5B,QAAQ,EACR,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,7 @@
1
+ export { QUOTE_FORM_PLACEMENTS, QUOTE_FORM_PLACEMENT_VARIANTS, QUOTE_FORM_VARIANTS, QuoteForm, createQuoteForm, } from './quote-form.js';
2
+ export { QuoteDialog } from './quote-dialog.js';
3
+ export { PhotoUpload } from './photo-upload.js';
4
+ export { Dialog } from './dialog.js';
5
+ export { Field } from './field.js';
6
+ export { THANK_YOU_DEFAULT_REPLY_TIME, ThankYou, thankYouMetadata, } from './thank-you.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/ui-kit/form/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,6BAA6B,EAC7B,mBAAmB,EACnB,SAAS,EACT,eAAe,GAYhB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,WAAW,EAA+C,MAAM,mBAAmB,CAAA;AAC5F,OAAO,EAAE,WAAW,EAAqE,MAAM,mBAAmB,CAAA;AAClH,OAAO,EAAE,MAAM,EAAsD,MAAM,aAAa,CAAA;AACxF,OAAO,EAAE,KAAK,EAA2E,MAAM,YAAY,CAAA;AAC3G,OAAO,EACL,4BAA4B,EAC5B,QAAQ,EACR,gBAAgB,GAIjB,MAAM,gBAAgB,CAAA","sourcesContent":["export {\n QUOTE_FORM_PLACEMENTS,\n QUOTE_FORM_PLACEMENT_VARIANTS,\n QUOTE_FORM_VARIANTS,\n QuoteForm,\n createQuoteForm,\n type BoundQuoteDialogProps,\n type BoundQuoteForm,\n type BoundQuoteFormProps,\n type CreateQuoteFormOptions,\n type QuoteFormCopy,\n type QuoteFormLayout,\n type QuoteFormPlacement,\n type QuoteFormProps,\n type QuoteFormSkin,\n type QuoteFormSlot,\n type QuoteFormVariant,\n} from './quote-form.js'\nexport { QuoteDialog, type QuoteDialogProps, type QuoteDialogSkin } from './quote-dialog.js'\nexport { PhotoUpload, type PhotoUploadProps, type PhotoUploadSkin, type PhotoUploadSlot } from './photo-upload.js'\nexport { Dialog, type DialogProps, type DialogSkin, type DialogSlot } from './dialog.js'\nexport { Field, type FieldControlProps, type FieldProps, type FieldSkin, type FieldSlot } from './field.js'\nexport {\n THANK_YOU_DEFAULT_REPLY_TIME,\n ThankYou,\n thankYouMetadata,\n type ThankYouProps,\n type ThankYouSkin,\n type ThankYouSlot,\n} from './thank-you.js'\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `PhotoUpload`: the "Add photos" drop box over the shared photo preparation (`useBrandlabContactAttachments`). A
3
+ * real file input, visually hidden but focusable, so the keyboard opens the picker with Enter or Space; the whole
4
+ * box is its label and a drop target. `accept="image/*"` and never a `capture` attribute, so phones offer the
5
+ * library as well as the camera. The "0 of 5" counter is a `HeadlessStatus`, so each change is announced. Chosen
6
+ * photos show as thumbnails with a remove button each.
7
+ */
8
+ import type { BrandlabContactAttachmentErrorReason, UseBrandlabContactAttachmentsResult } from '../../contact/attachments-client.js';
9
+ import { type SkinEntry } from '../skin.js';
10
+ export type PhotoUploadSlot = 'field' | 'label' | 'drop' | 'dropIcon' | 'counter' | 'photos' | 'photo' | 'thumb' | 'remove' | 'error';
11
+ export type PhotoUploadSkin = SkinEntry<PhotoUploadSlot>;
12
+ export interface PhotoUploadProps extends PhotoUploadSkin {
13
+ /** The file input's id. */
14
+ readonly id: string;
15
+ /** The drop box's label, for example "Add photos". */
16
+ readonly label: string;
17
+ /** The prepared photos, from `useBrandlabContactForm().attachments` or `useBrandlabContactAttachments()`. */
18
+ readonly attachments: UseBrandlabContactAttachmentsResult;
19
+ /** The counter, with `{count}` and `{max}`, for example "{count} of {max}". */
20
+ readonly counter: string;
21
+ /** Each remove button's name, with `{number}`, for example "Remove photo {number}". */
22
+ readonly removeLabel: string;
23
+ /** `image/*` by default; `image/*,application/pdf` where the contract takes PDFs. */
24
+ readonly accept?: string;
25
+ /**
26
+ * The site's message for each reason a photo is refused (`count` may use `{max}`). Without them the shared photo
27
+ * preparation's own wording shows.
28
+ */
29
+ readonly errorMessages?: Readonly<Record<BrandlabContactAttachmentErrorReason, string>> | undefined;
30
+ /** A form-level message, for example when photos are required. A refused photo's message wins. */
31
+ readonly error?: string | undefined;
32
+ readonly required?: boolean;
33
+ /** Locks the box while an enquiry is sending. */
34
+ readonly disabled?: boolean;
35
+ }
36
+ export declare function PhotoUpload({ id, label, attachments, counter, removeLabel, accept, errorMessages, error, required, disabled, classes, }: PhotoUploadProps): import("react/jsx-runtime").JSX.Element;
37
+ //# sourceMappingURL=photo-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"photo-upload.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/photo-upload.tsx"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,oCAAoC,EACpC,mCAAmC,EACpC,MAAM,qCAAqC,CAAA;AAG5C,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAGjD,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,OAAO,GACP,MAAM,GACN,UAAU,GACV,SAAS,GACT,QAAQ,GACR,OAAO,GACP,OAAO,GACP,QAAQ,GACR,OAAO,CAAA;AACX,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,eAAe,CAAC,CAAA;AAExD,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,6GAA6G;IAC7G,QAAQ,CAAC,WAAW,EAAE,mCAAmC,CAAA;IACzD,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,uFAAuF;IACvF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,qFAAqF;IACrF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,oCAAoC,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;IACnG,kGAAkG;IAClG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,KAAK,EACL,WAAW,EACX,OAAO,EACP,WAAW,EACX,MAAkB,EAClB,aAAa,EACb,KAAK,EACL,QAAgB,EAChB,QAAgB,EAChB,OAAO,GACR,EAAE,gBAAgB,2CAgElB"}
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { createElement as _createElement } from "react";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ import { HeadlessStatus } from '../../ui.js';
5
+ import { Icon } from '../icons.js';
6
+ import { slot } from '../skin.js';
7
+ import { fillCopy } from './copy.js';
8
+ export function PhotoUpload({ id, label, attachments, counter, removeLabel, accept = 'image/*', errorMessages, error, required = false, disabled = false, classes, }) {
9
+ const counterId = `${id}-count`;
10
+ const errorId = `${id}-error`;
11
+ const reason = attachments.errorReason;
12
+ const refused = reason && errorMessages ? fillCopy(errorMessages[reason], { max: attachments.maxImages }) : attachments.errorMessage;
13
+ const message = refused ?? error;
14
+ const state = attachments.isProcessing ? 'busy' : attachments.isDragging ? 'dragging' : 'idle';
15
+ return (_jsxs("div", { "data-bl": "photo-upload", ...slot(classes, 'field'), children: [_jsxs("div", { ...slot(classes, 'drop'), "data-state": state, "data-disabled": disabled ? 'true' : undefined, onDragOver: disabled ? undefined : attachments.handleDragOver, onDragLeave: disabled ? undefined : attachments.handleDragLeave, onDrop: disabled ? undefined : (event) => void attachments.handleDrop(event), children: [_jsx("input", { id: id, type: "file", accept: accept, multiple: true, disabled: disabled, "aria-required": required ? true : undefined, "aria-invalid": message ? true : undefined, "aria-describedby": [counterId, message ? errorId : undefined].filter(Boolean).join(' '), "data-bl-visually-hidden": "", onChange: (event) => void attachments.handleImageChange(event) }), _jsxs("label", { ...slot(classes, 'label'), htmlFor: id, children: [_jsx("span", { ...slot(classes, 'dropIcon'), children: _jsx(Icon, { name: "camera" }) }), _jsx("span", { children: label })] }), _jsx(HeadlessStatus, { ...slot(classes, 'counter'), id: counterId, children: fillCopy(counter, { count: attachments.images.length, max: attachments.maxImages }) })] }), attachments.images.length > 0 ? (_jsx("ul", { ...slot(classes, 'photos'), children: attachments.images.map((image, index) => (_createElement("li", { ...slot(classes, 'photo'), key: image.preview },
16
+ _jsx("img", { ...slot(classes, 'thumb'), src: image.preview, alt: "", width: 64, height: 64 }),
17
+ _jsxs("button", { ...slot(classes, 'remove'), type: "button", disabled: disabled, onClick: () => attachments.removeImage(index), children: [_jsx(Icon, { name: "close" }), _jsx("span", { "data-bl-visually-hidden": "", children: fillCopy(removeLabel, { number: index + 1 }) })] })))) })) : null, message ? (_jsx("div", { ...slot(classes, 'error'), id: errorId, role: "alert", children: message })) : null] }));
18
+ }
19
+ //# sourceMappingURL=photo-upload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"photo-upload.js","sourceRoot":"","sources":["../../../src/ui-kit/form/photo-upload.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * `PhotoUpload`: the \"Add photos\" drop box over the shared photo preparation (`useBrandlabContactAttachments`). A\n * real file input, visually hidden but focusable, so the keyboard opens the picker with Enter or Space; the whole\n * box is its label and a drop target. `accept=\"image/*\"` and never a `capture` attribute, so phones offer the\n * library as well as the camera. The \"0 of 5\" counter is a `HeadlessStatus`, so each change is announced. Chosen\n * photos show as thumbnails with a remove button each.\n */\nimport type {\n BrandlabContactAttachmentErrorReason,\n UseBrandlabContactAttachmentsResult,\n} from '../../contact/attachments-client.js'\nimport { HeadlessStatus } from '../../ui.js'\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport { fillCopy } from './copy.js'\n\nexport type PhotoUploadSlot =\n | 'field'\n | 'label'\n | 'drop'\n | 'dropIcon'\n | 'counter'\n | 'photos'\n | 'photo'\n | 'thumb'\n | 'remove'\n | 'error'\nexport type PhotoUploadSkin = SkinEntry<PhotoUploadSlot>\n\nexport interface PhotoUploadProps extends PhotoUploadSkin {\n /** The file input's id. */\n readonly id: string\n /** The drop box's label, for example \"Add photos\". */\n readonly label: string\n /** The prepared photos, from `useBrandlabContactForm().attachments` or `useBrandlabContactAttachments()`. */\n readonly attachments: UseBrandlabContactAttachmentsResult\n /** The counter, with `{count}` and `{max}`, for example \"{count} of {max}\". */\n readonly counter: string\n /** Each remove button's name, with `{number}`, for example \"Remove photo {number}\". */\n readonly removeLabel: string\n /** `image/*` by default; `image/*,application/pdf` where the contract takes PDFs. */\n readonly accept?: string\n /**\n * The site's message for each reason a photo is refused (`count` may use `{max}`). Without them the shared photo\n * preparation's own wording shows.\n */\n readonly errorMessages?: Readonly<Record<BrandlabContactAttachmentErrorReason, string>> | undefined\n /** A form-level message, for example when photos are required. A refused photo's message wins. */\n readonly error?: string | undefined\n readonly required?: boolean\n /** Locks the box while an enquiry is sending. */\n readonly disabled?: boolean\n}\n\nexport function PhotoUpload({\n id,\n label,\n attachments,\n counter,\n removeLabel,\n accept = 'image/*',\n errorMessages,\n error,\n required = false,\n disabled = false,\n classes,\n}: PhotoUploadProps) {\n const counterId = `${id}-count`\n const errorId = `${id}-error`\n const reason = attachments.errorReason\n const refused = reason && errorMessages ? fillCopy(errorMessages[reason], { max: attachments.maxImages }) : attachments.errorMessage\n const message = refused ?? error\n const state = attachments.isProcessing ? 'busy' : attachments.isDragging ? 'dragging' : 'idle'\n return (\n <div data-bl=\"photo-upload\" {...slot(classes, 'field')}>\n <div\n {...slot(classes, 'drop')}\n data-state={state}\n data-disabled={disabled ? 'true' : undefined}\n onDragOver={disabled ? undefined : attachments.handleDragOver}\n onDragLeave={disabled ? undefined : attachments.handleDragLeave}\n onDrop={disabled ? undefined : (event) => void attachments.handleDrop(event)}\n >\n <input\n id={id}\n type=\"file\"\n accept={accept}\n multiple\n disabled={disabled}\n aria-required={required ? true : undefined}\n aria-invalid={message ? true : undefined}\n aria-describedby={[counterId, message ? errorId : undefined].filter(Boolean).join(' ')}\n data-bl-visually-hidden=\"\"\n onChange={(event) => void attachments.handleImageChange(event)}\n />\n <label {...slot(classes, 'label')} htmlFor={id}>\n <span {...slot(classes, 'dropIcon')}>\n <Icon name=\"camera\" />\n </span>\n <span>{label}</span>\n </label>\n <HeadlessStatus {...slot(classes, 'counter')} id={counterId}>\n {fillCopy(counter, { count: attachments.images.length, max: attachments.maxImages })}\n </HeadlessStatus>\n </div>\n {attachments.images.length > 0 ? (\n <ul {...slot(classes, 'photos')}>\n {attachments.images.map((image, index) => (\n <li {...slot(classes, 'photo')} key={image.preview}>\n <img {...slot(classes, 'thumb')} src={image.preview} alt=\"\" width={64} height={64} />\n <button\n {...slot(classes, 'remove')}\n type=\"button\"\n disabled={disabled}\n onClick={() => attachments.removeImage(index)}\n >\n <Icon name=\"close\" />\n <span data-bl-visually-hidden=\"\">{fillCopy(removeLabel, { number: index + 1 })}</span>\n </button>\n </li>\n ))}\n </ul>\n ) : null}\n {message ? (\n <div {...slot(classes, 'error')} id={errorId} role=\"alert\">\n {message}\n </div>\n ) : null}\n </div>\n )\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type DialogSkin } from './dialog.js';
3
+ export type QuoteDialogSkin = DialogSkin;
4
+ export interface QuoteDialogProps extends QuoteDialogSkin {
5
+ /** The dialog's H2, for example "Get a free quote". */
6
+ readonly title: string;
7
+ /** The close button's accessible name, for example "Close". */
8
+ readonly closeLabel: string;
9
+ /** The dialog's id (default `quote-dialog-panel`; the form inside is `quote-dialog`). */
10
+ readonly id?: string;
11
+ /** The form, rendered once the dialog first opens. */
12
+ readonly children: ReactNode;
13
+ }
14
+ export declare function QuoteDialog({ title, closeLabel, id, classes, children }: QuoteDialogProps): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=quote-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-dialog.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/quote-dialog.tsx"],"names":[],"mappings":"AAWA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACnE,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAErD,MAAM,MAAM,eAAe,GAAG,UAAU,CAAA;AAExC,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B;AA0BD,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAyB,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CAwChH"}
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * `QuoteDialog`: the quote form in a modal `Dialog`, opened by any `QuoteButton` (`[data-bl="quote-button"]`) on the
5
+ * page through one delegated click listener while the dialog is mounted. On the page the button links to (the form's
6
+ * own page, for example `/contact`) the link keeps its default and scrolls to the form. Without scripts every button
7
+ * stays a plain link to the form. The form mounts on the first open and stays mounted, so typed values survive
8
+ * closing and reopening; focus returns to the button that opened it. A client navigation (the consent link, for
9
+ * example) closes it.
10
+ */
11
+ import { usePathname } from 'next/navigation';
12
+ import { useEffect, useRef, useState } from 'react';
13
+ import { Dialog } from './dialog.js';
14
+ function samePath(left, right) {
15
+ const trim = (path) => path.replace(/\/+$/, '') || '/';
16
+ return trim(left) === trim(right);
17
+ }
18
+ /** The quote button a click landed on, when the click should open the dialog. */
19
+ function quoteButtonFor(event) {
20
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
21
+ return undefined;
22
+ }
23
+ const target = event.target instanceof Element ? event.target.closest('a[data-bl="quote-button"]') : null;
24
+ if (!(target instanceof HTMLAnchorElement) || (target.target && target.target !== '_self'))
25
+ return undefined;
26
+ let url;
27
+ try {
28
+ url = new URL(target.href, window.location.href);
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ if (url.origin !== window.location.origin)
34
+ return undefined;
35
+ // On the form's own page the link scrolls to the form instead.
36
+ if (samePath(url.pathname, window.location.pathname))
37
+ return undefined;
38
+ return target;
39
+ }
40
+ export function QuoteDialog({ title, closeLabel, id = 'quote-dialog-panel', classes, children }) {
41
+ const [open, setOpen] = useState(false);
42
+ const [mounted, setMounted] = useState(false);
43
+ const returnFocusRef = useRef(null);
44
+ const pathname = usePathname();
45
+ const shownPath = useRef(pathname);
46
+ useEffect(() => {
47
+ if (shownPath.current === pathname)
48
+ return;
49
+ shownPath.current = pathname;
50
+ setOpen(false);
51
+ }, [pathname]);
52
+ useEffect(() => {
53
+ // Capture phase: decided before a framework link handler starts a client navigation.
54
+ const onClick = (event) => {
55
+ const button = quoteButtonFor(event);
56
+ if (!button)
57
+ return;
58
+ event.preventDefault();
59
+ returnFocusRef.current = button;
60
+ setMounted(true);
61
+ setOpen(true);
62
+ };
63
+ document.addEventListener('click', onClick, true);
64
+ return () => document.removeEventListener('click', onClick, true);
65
+ }, []);
66
+ return (_jsx(Dialog, { id: id, open: open, onClose: () => setOpen(false), title: title, closeLabel: closeLabel, returnFocusRef: returnFocusRef, classes: classes, children: mounted ? children : null }));
67
+ }
68
+ //# sourceMappingURL=quote-dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-dialog.js","sourceRoot":"","sources":["../../../src/ui-kit/form/quote-dialog.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * `QuoteDialog`: the quote form in a modal `Dialog`, opened by any `QuoteButton` (`[data-bl=\"quote-button\"]`) on the\n * page through one delegated click listener while the dialog is mounted. On the page the button links to (the form's\n * own page, for example `/contact`) the link keeps its default and scrolls to the form. Without scripts every button\n * stays a plain link to the form. The form mounts on the first open and stays mounted, so typed values survive\n * closing and reopening; focus returns to the button that opened it. A client navigation (the consent link, for\n * example) closes it.\n */\nimport { usePathname } from 'next/navigation'\nimport { useEffect, useRef, useState, type ReactNode } from 'react'\nimport { Dialog, type DialogSkin } from './dialog.js'\n\nexport type QuoteDialogSkin = DialogSkin\n\nexport interface QuoteDialogProps extends QuoteDialogSkin {\n /** The dialog's H2, for example \"Get a free quote\". */\n readonly title: string\n /** The close button's accessible name, for example \"Close\". */\n readonly closeLabel: string\n /** The dialog's id (default `quote-dialog-panel`; the form inside is `quote-dialog`). */\n readonly id?: string\n /** The form, rendered once the dialog first opens. */\n readonly children: ReactNode\n}\n\nfunction samePath(left: string, right: string) {\n const trim = (path: string) => path.replace(/\\/+$/, '') || '/'\n return trim(left) === trim(right)\n}\n\n/** The quote button a click landed on, when the click should open the dialog. */\nfunction quoteButtonFor(event: MouseEvent): HTMLAnchorElement | undefined {\n if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {\n return undefined\n }\n const target = event.target instanceof Element ? event.target.closest('a[data-bl=\"quote-button\"]') : null\n if (!(target instanceof HTMLAnchorElement) || (target.target && target.target !== '_self')) return undefined\n let url: URL\n try {\n url = new URL(target.href, window.location.href)\n } catch {\n return undefined\n }\n if (url.origin !== window.location.origin) return undefined\n // On the form's own page the link scrolls to the form instead.\n if (samePath(url.pathname, window.location.pathname)) return undefined\n return target\n}\n\nexport function QuoteDialog({ title, closeLabel, id = 'quote-dialog-panel', classes, children }: QuoteDialogProps) {\n const [open, setOpen] = useState(false)\n const [mounted, setMounted] = useState(false)\n const returnFocusRef = useRef<HTMLElement | null>(null)\n const pathname = usePathname()\n const shownPath = useRef(pathname)\n\n useEffect(() => {\n if (shownPath.current === pathname) return\n shownPath.current = pathname\n setOpen(false)\n }, [pathname])\n\n useEffect(() => {\n // Capture phase: decided before a framework link handler starts a client navigation.\n const onClick = (event: MouseEvent) => {\n const button = quoteButtonFor(event)\n if (!button) return\n event.preventDefault()\n returnFocusRef.current = button\n setMounted(true)\n setOpen(true)\n }\n document.addEventListener('click', onClick, true)\n return () => document.removeEventListener('click', onClick, true)\n }, [])\n\n return (\n <Dialog\n id={id}\n open={open}\n onClose={() => setOpen(false)}\n title={title}\n closeLabel={closeLabel}\n returnFocusRef={returnFocusRef}\n classes={classes}\n >\n {mounted ? children : null}\n </Dialog>\n )\n}\n"]}