@ticketboothapp/booking 1.2.24 → 1.2.25-rc.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 (158) hide show
  1. package/package.json +29 -2
  2. package/src/assets/icons/minus.svg +7 -0
  3. package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
  4. package/src/assets/icons/plus.svg +3 -0
  5. package/src/colours.css +23 -0
  6. package/src/components/BookingDetails.module.css +1591 -0
  7. package/src/components/BookingDetails.tsx +2264 -0
  8. package/src/components/BookingWidget.tsx +302 -0
  9. package/src/components/ManageBookingView.tsx +437 -0
  10. package/src/components/PhoneInputWithCountry.module.css +131 -0
  11. package/src/components/PhoneInputWithCountry.tsx +44 -0
  12. package/src/components/PickupLocationDialog.module.css +360 -0
  13. package/src/components/PickupLocationDialog.tsx +357 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/booking/AddOnsSection.module.css +10 -0
  17. package/src/components/booking/AddOnsSection.tsx +184 -0
  18. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  19. package/src/components/booking/BookingDialog.module.css +643 -0
  20. package/src/components/booking/BookingDialog.tsx +356 -0
  21. package/src/components/booking/BookingFlow.tsx +4385 -0
  22. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  23. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  24. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  25. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  26. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  27. package/src/components/booking/BookingProductGrid.module.css +359 -0
  28. package/src/components/booking/BookingProductGrid.tsx +497 -0
  29. package/src/components/booking/Calendar.module.css +616 -0
  30. package/src/components/booking/Calendar.tsx +1123 -0
  31. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  32. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  33. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  34. package/src/components/booking/CheckoutForm.module.css +244 -0
  35. package/src/components/booking/CheckoutForm.tsx +364 -0
  36. package/src/components/booking/CheckoutModal.tsx +451 -0
  37. package/src/components/booking/CurrencySwitcher.tsx +81 -0
  38. package/src/components/booking/DapFlowCollage.tsx +88 -0
  39. package/src/components/booking/DapTourDescription.tsx +35 -0
  40. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  41. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  42. package/src/components/booking/ErrorBoundary.tsx +63 -0
  43. package/src/components/booking/InfoTooltip.tsx +108 -0
  44. package/src/components/booking/ItineraryBox.module.css +258 -0
  45. package/src/components/booking/ItineraryBox.tsx +550 -0
  46. package/src/components/booking/ItineraryBuilder.tsx +82 -0
  47. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  48. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  49. package/src/components/booking/MealDrinkAddOnSelector.tsx +338 -0
  50. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  51. package/src/components/booking/PickupLocationSelector.tsx +1566 -0
  52. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  53. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  54. package/src/components/booking/PriceBreakdown.tsx +154 -0
  55. package/src/components/booking/PriceSummary.tsx +234 -0
  56. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  57. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  58. package/src/components/booking/PromoCodeInput.module.css +166 -0
  59. package/src/components/booking/PromoCodeInput.tsx +99 -0
  60. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  61. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  62. package/src/components/booking/TermsAcceptance.tsx +111 -0
  63. package/src/components/booking/TicketSelector.module.css +164 -0
  64. package/src/components/booking/TicketSelector.tsx +199 -0
  65. package/src/components/booking/TourDescription.module.css +304 -0
  66. package/src/components/booking/TourDescription.tsx +273 -0
  67. package/src/components/booking/booking-flow-ui.ts +38 -0
  68. package/src/components/booking/booking-flow.css +944 -0
  69. package/src/components/button.css +245 -0
  70. package/src/components/button.tsx +152 -0
  71. package/src/components/colorable-svg.tsx +29 -0
  72. package/src/components/image.css +29 -0
  73. package/src/components/image.tsx +113 -0
  74. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  75. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  76. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +45 -0
  77. package/src/components/product-tag.module.css +30 -0
  78. package/src/components/product-tag.tsx +34 -0
  79. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  80. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  81. package/src/components/terms/TermsContent.tsx +178 -0
  82. package/src/components/value-pill.module.css +59 -0
  83. package/src/components/value-pill.tsx +46 -0
  84. package/src/constants/images.ts +556 -0
  85. package/src/constants/pill-values.ts +210 -0
  86. package/src/constants/products.ts +155 -0
  87. package/src/contexts/AvailabilitiesCacheContext.tsx +125 -0
  88. package/src/contexts/BookingAppContext.tsx +134 -0
  89. package/src/contexts/CompanyContext.tsx +70 -0
  90. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  91. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  92. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  93. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  94. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  95. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  96. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  97. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  98. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  99. package/src/data/product-descriptions/private-tour.en.json +80 -0
  100. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  101. package/src/data/products-config.json +101 -0
  102. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  103. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  104. package/src/index.ts +79 -0
  105. package/src/lib/analytics.ts +197 -0
  106. package/src/lib/booking/booking-source.ts +51 -0
  107. package/src/lib/booking/checkout-breakdown.ts +69 -0
  108. package/src/lib/booking/correlation-id.ts +46 -0
  109. package/src/lib/booking/i18n/config.ts +21 -0
  110. package/src/lib/booking/i18n/index.tsx +144 -0
  111. package/src/lib/booking/i18n/messages/en.json +236 -0
  112. package/src/lib/booking/i18n/messages/fr.json +236 -0
  113. package/src/lib/booking/itinerary-display.ts +36 -0
  114. package/src/lib/booking/itinerary-labels.ts +70 -0
  115. package/src/lib/booking/location-calculations.ts +43 -0
  116. package/src/lib/booking/location-utils.ts +165 -0
  117. package/src/lib/booking/map-utils.ts +153 -0
  118. package/src/lib/booking/marker-icons.ts +113 -0
  119. package/src/lib/booking/normalize-booking-product-id.ts +21 -0
  120. package/src/lib/booking/pickup-location-types.ts +25 -0
  121. package/src/lib/booking/places-api.ts +154 -0
  122. package/src/lib/booking/pricing.ts +466 -0
  123. package/src/lib/booking/product-option-id.ts +35 -0
  124. package/src/lib/booking/source-metadata.ts +226 -0
  125. package/src/lib/booking/sunday-week.ts +14 -0
  126. package/src/lib/booking/theme.ts +83 -0
  127. package/src/lib/booking/trace-context.ts +62 -0
  128. package/src/lib/booking/utils.ts +9 -0
  129. package/src/lib/booking-api.ts +1793 -0
  130. package/src/lib/booking-constants.ts +23 -0
  131. package/src/lib/booking-ref.ts +13 -0
  132. package/src/lib/booking-types.ts +36 -0
  133. package/src/lib/currency.ts +81 -0
  134. package/src/lib/dap-descriptions.ts +50 -0
  135. package/src/lib/dap-itinerary-preview.ts +315 -0
  136. package/src/lib/dependent-add-on-api.ts +434 -0
  137. package/src/lib/env.ts +96 -0
  138. package/src/lib/firebase.ts +20 -0
  139. package/src/lib/job-application-api.ts +83 -0
  140. package/src/lib/manage-booking-embed-print.ts +16 -0
  141. package/src/lib/manage-booking-post-checkout.ts +68 -0
  142. package/src/lib/photo-dap-config.ts +228 -0
  143. package/src/lib/photo-packages.ts +75 -0
  144. package/src/lib/pickup/map-utils.ts +56 -0
  145. package/src/lib/pickup/marker-icons.ts +19 -0
  146. package/src/lib/product-descriptions.ts +66 -0
  147. package/src/lib/products-config.ts +73 -0
  148. package/src/providers/booking-dialog-provider.tsx +282 -0
  149. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  150. package/src/radius.css +5 -0
  151. package/src/spacing.css +7 -0
  152. package/src/strings/en.json +1774 -0
  153. package/src/strings/es.json +1573 -0
  154. package/src/strings/fr.json +1573 -0
  155. package/src/strings/index.js +23 -0
  156. package/src/text-style.css +56 -0
  157. package/src/utils/currency-converter.ts +101 -0
  158. package/tsconfig.json +8 -2
@@ -0,0 +1,226 @@
1
+ export {
2
+ KnownBookingSource,
3
+ DEFAULT_BOOKING_SOURCE,
4
+ PARTNER_PORTAL_BOOKING_SOURCE,
5
+ inferClientBookingSourceFromProductIds,
6
+ mergedMetadataImpliesPartnerPortal,
7
+ } from '@/lib/booking/booking-source';
8
+
9
+ import { KnownBookingSource, mergedMetadataImpliesPartnerPortal } from '@/lib/booking/booking-source';
10
+
11
+ export interface BookingSourceMetadata {
12
+ pageUrl?: string;
13
+ pagePath?: string;
14
+ pageQuery?: string;
15
+ referrerUrl?: string;
16
+ utmSource?: string;
17
+ utmMedium?: string;
18
+ utmCampaign?: string;
19
+ utmTerm?: string;
20
+ utmContent?: string;
21
+ gclid?: string;
22
+ fbclid?: string;
23
+ msclkid?: string;
24
+ partnerId?: string;
25
+ partnerSlug?: string;
26
+ agentId?: string;
27
+ agentName?: string;
28
+ }
29
+
30
+ function withDefinedValues<T extends object>(obj: T): Partial<T> {
31
+ return Object.fromEntries(
32
+ Object.entries(obj).filter(([, value]) => value !== undefined && value !== null && value !== ''),
33
+ ) as Partial<T>;
34
+ }
35
+
36
+ /**
37
+ * Public marketing embed routes on the main site (`/partner/{slug}`). Used to decide when
38
+ * `partnerId` in the query string is intentional vs noisy on generic tour/product URLs.
39
+ */
40
+ export function isPublicPartnerMarketingPath(pathname: string): boolean {
41
+ return /^\/partner\/[^/]+/i.test(pathname.trim());
42
+ }
43
+
44
+ /** Partner booking SPA (`booking.*` prod/staging) — `?partnerId=` is intentional on `/` or any path. */
45
+ export function isDedicatedPartnerBookingPortalHost(hostname: string): boolean {
46
+ const h = hostname.trim().toLowerCase();
47
+ return h === 'booking.viaviamorainelake.com' || h === 'staging.booking.viaviamorainelake.com';
48
+ }
49
+
50
+ /**
51
+ * Reads the **current browser URL** (and referrer) into attribution fields. Supplements
52
+ * `mergeBookingSourceMetadata` with `canonicalAttribution` from the route so identity does
53
+ * not depend on the visible URL alone.
54
+ *
55
+ * **`partnerId` from `?partnerId=`** is included when:
56
+ * - the path is **`/partner/:slug`** on the **main marketing site**, or
57
+ * - the host is the **dedicated partner booking app** (`booking.*` prod/staging).
58
+ *
59
+ * On other generic marketing pages we still capture UTMs and full `pageUrl`, but omit loose
60
+ * `partnerId` query params so promo + backend rules aren’t overshadowed by “explicit” metadata.
61
+ */
62
+ export function buildBookingSourceMetadataFromLocation(): Partial<BookingSourceMetadata> {
63
+ if (typeof window === 'undefined') {
64
+ return {};
65
+ }
66
+
67
+ const currentUrl = new URL(window.location.href);
68
+ const params = currentUrl.searchParams;
69
+ const partnerMatch = currentUrl.pathname.match(/^\/partner\/([^/]+)/i);
70
+ const onPartnerEmbedPath = isPublicPartnerMarketingPath(currentUrl.pathname);
71
+ const onPortalHost = isDedicatedPartnerBookingPortalHost(currentUrl.hostname);
72
+ const partnerFromQuery =
73
+ onPartnerEmbedPath || onPortalHost ? params.get('partnerId') || undefined : undefined;
74
+ const partnerSlugFromPath = partnerMatch?.[1];
75
+ const agentIdFromQuery = params.get('agentId') || undefined;
76
+ const agentNameFromQuery = params.get('agentName') || undefined;
77
+
78
+ return withDefinedValues<BookingSourceMetadata>({
79
+ pageUrl: currentUrl.href,
80
+ pagePath: currentUrl.pathname,
81
+ pageQuery: currentUrl.search || undefined,
82
+ referrerUrl: document.referrer || undefined,
83
+ utmSource: params.get('utm_source') || undefined,
84
+ utmMedium: params.get('utm_medium') || undefined,
85
+ utmCampaign: params.get('utm_campaign') || undefined,
86
+ utmTerm: params.get('utm_term') || undefined,
87
+ utmContent: params.get('utm_content') || undefined,
88
+ gclid: params.get('gclid') || undefined,
89
+ fbclid: params.get('fbclid') || undefined,
90
+ msclkid: params.get('msclkid') || undefined,
91
+ partnerId: partnerFromQuery,
92
+ partnerSlug: partnerSlugFromPath,
93
+ agentId: agentIdFromQuery,
94
+ agentName: agentNameFromQuery,
95
+ });
96
+ }
97
+
98
+ /** Later layers override earlier ones (undefined / null / empty string in a layer are omitted). */
99
+ export function mergeBookingSourceMetadata(
100
+ ...layers: Array<Partial<BookingSourceMetadata> | null | undefined>
101
+ ): Partial<BookingSourceMetadata> {
102
+ let acc: Partial<BookingSourceMetadata> = {};
103
+ for (const layer of layers) {
104
+ if (!layer) continue;
105
+ acc = { ...acc, ...withDefinedValues(layer as BookingSourceMetadata) };
106
+ }
107
+ return acc;
108
+ }
109
+
110
+ function hostnameForSourceDecision(merged: Partial<BookingSourceMetadata>): string {
111
+ if (typeof window !== 'undefined') {
112
+ try {
113
+ return new URL(window.location.href).hostname.toLowerCase();
114
+ } catch {
115
+ return '';
116
+ }
117
+ }
118
+ const pu = merged.pageUrl?.trim();
119
+ if (!pu) return '';
120
+ try {
121
+ return new URL(pu).hostname.toLowerCase();
122
+ } catch {
123
+ return '';
124
+ }
125
+ }
126
+
127
+ export type BuildBookingSourceContextOptions = {
128
+ /**
129
+ * When metadata does not imply partner portal, use this channel (GYG/VIATOR/WEBSITE from product ids).
130
+ * AFFILIATE/DASHBOARD are normally set server-side from request headers.
131
+ */
132
+ clientChannelSource?: KnownBookingSource;
133
+ /** Dedicated partner booking app (`booking.*`): persist `source` as PARTNER_PORTAL (not PUBLIC_PARTNER_WEBSITE). */
134
+ forcePartnerPortalChannel?: boolean;
135
+ /** TicketBooth provider dashboard embed (`BookingAppProvider` mode `provider-dashboard`): persist `source` as DASHBOARD. */
136
+ forceDashboardSource?: boolean;
137
+ };
138
+
139
+ /**
140
+ * Builds reserve/checkout `source` + `sourceMetadata` from composed metadata. Partner portal is
141
+ * inferred from `par_` org id or a valid marketing `partnerSlug` (including route-level canonical),
142
+ * not from hostname alone. Optional `clientChannelSource` supplies GYG/VIATOR/WEBSITE otherwise.
143
+ */
144
+ export function buildBookingSourceContext(
145
+ mergedMetadata: Partial<BookingSourceMetadata>,
146
+ options?: BuildBookingSourceContextOptions,
147
+ ): {
148
+ source: KnownBookingSource;
149
+ sourceMetadata?: BookingSourceMetadata;
150
+ source_metadata?: BookingSourceMetadata;
151
+ } {
152
+ const merged = withDefinedValues(mergedMetadata as BookingSourceMetadata);
153
+ const channel = options?.clientChannelSource ?? KnownBookingSource.WEBSITE;
154
+
155
+ if (options?.forceDashboardSource === true) {
156
+ const hasMeta = Object.keys(merged).length > 0;
157
+ const context: {
158
+ source: KnownBookingSource;
159
+ sourceMetadata?: BookingSourceMetadata;
160
+ source_metadata?: BookingSourceMetadata;
161
+ } = {
162
+ source: KnownBookingSource.DASHBOARD,
163
+ ...(hasMeta
164
+ ? {
165
+ sourceMetadata: merged as BookingSourceMetadata,
166
+ source_metadata: merged as BookingSourceMetadata,
167
+ }
168
+ : {}),
169
+ };
170
+ try {
171
+ const md = context.sourceMetadata ?? context.source_metadata;
172
+ console.info(
173
+ `[booking-source-debug] built-context summary source=${context.source} path=${md?.pagePath ?? 'n/a'} partner=${md?.partnerId ?? 'n/a'} partner_slug=${md?.partnerSlug ?? 'n/a'} utm_source=${md?.utmSource ?? 'n/a'} utm_medium=${md?.utmMedium ?? 'n/a'} utm_campaign=${md?.utmCampaign ?? 'n/a'}`,
174
+ );
175
+ console.info(`[booking-source-debug] built-context payload\n${JSON.stringify(context, null, 2)}`);
176
+ } catch {
177
+ console.info('[booking-source-debug] built-context', context);
178
+ }
179
+ return context;
180
+ }
181
+
182
+ if (Object.keys(merged).length === 0) {
183
+ return { source: channel };
184
+ }
185
+
186
+ const host = hostnameForSourceDecision(merged);
187
+ const dedicatedPartnerBookingHost =
188
+ host === 'booking.viaviamorainelake.com' ||
189
+ host === 'staging.booking.viaviamorainelake.com';
190
+ const pid = typeof merged.partnerId === 'string' ? merged.partnerId.trim() : '';
191
+ const pslug = typeof merged.partnerSlug === 'string' ? merged.partnerSlug.trim() : '';
192
+
193
+ const forceDedicatedPortal =
194
+ options?.forcePartnerPortalChannel === true ||
195
+ (dedicatedPartnerBookingHost && (pid.length > 0 || pslug.length > 0));
196
+ const mainSitePartnerEmbed =
197
+ !forceDedicatedPortal && mergedMetadataImpliesPartnerPortal(merged);
198
+
199
+ const source = forceDedicatedPortal
200
+ ? KnownBookingSource.PARTNER_PORTAL
201
+ : mainSitePartnerEmbed
202
+ ? KnownBookingSource.PUBLIC_PARTNER_WEBSITE
203
+ : channel;
204
+
205
+ const context: {
206
+ source: KnownBookingSource;
207
+ sourceMetadata?: BookingSourceMetadata;
208
+ source_metadata?: BookingSourceMetadata;
209
+ } = {
210
+ source,
211
+ sourceMetadata: merged as BookingSourceMetadata,
212
+ source_metadata: merged as BookingSourceMetadata,
213
+ };
214
+
215
+ try {
216
+ const md = context.sourceMetadata ?? context.source_metadata;
217
+ console.info(
218
+ `[booking-source-debug] built-context summary source=${context.source} path=${md?.pagePath ?? 'n/a'} partner=${md?.partnerId ?? 'n/a'} partner_slug=${md?.partnerSlug ?? 'n/a'} utm_source=${md?.utmSource ?? 'n/a'} utm_medium=${md?.utmMedium ?? 'n/a'} utm_campaign=${md?.utmCampaign ?? 'n/a'}`,
219
+ );
220
+ console.info(`[booking-source-debug] built-context payload\n${JSON.stringify(context, null, 2)}`);
221
+ } catch {
222
+ console.info('[booking-source-debug] built-context', context);
223
+ }
224
+
225
+ return context;
226
+ }
@@ -0,0 +1,14 @@
1
+ import { addDays, parseISO } from 'date-fns';
2
+ import { formatInTimeZone, fromZonedTime } from 'date-fns-tz';
3
+
4
+ /**
5
+ * Sunday (week start) of the week containing `dateStr` (yyyy-MM-dd), in `timezone`.
6
+ * Matches the booking calendar grid so fetch windows align with the visible week.
7
+ */
8
+ export function getSundayOfWeek(dateStr: string, timezone: string): string {
9
+ const noonInTz = fromZonedTime(parseISO(`${dateStr}T12:00:00`), timezone);
10
+ const isoDay = parseInt(formatInTimeZone(noonInTz, timezone, 'i'), 10); // 1=Mon, 7=Sun
11
+ const daysToSubtract = isoDay === 7 ? 0 : isoDay;
12
+ const sundayDate = addDays(noonInTz, -daysToSubtract);
13
+ return formatInTimeZone(sundayDate, timezone, 'yyyy-MM-dd');
14
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Booking UI theme: host can override these to get a different skin (fonts, colours, etc.)
3
+ * while keeping the same bones. All values are applied as CSS variables (--booking-*) on the
4
+ * booking root so components use var(--booking-*) and pick up host overrides.
5
+ */
6
+ export interface BookingTheme {
7
+ /** Primary brand colour (buttons, links, accents). */
8
+ primary: string;
9
+ /** Primary hover/dark state. */
10
+ primaryHover: string;
11
+ /** Page background (gradient start). */
12
+ bg: string;
13
+ /** Page background gradient end. */
14
+ bgEnd: string;
15
+ /** Card/surface background. */
16
+ surface: string;
17
+ /** Header/footer dark background. */
18
+ headerBg: string;
19
+ /** Header text (usually white on headerBg). */
20
+ headerText: string;
21
+ /** Body text. */
22
+ text: string;
23
+ /** Muted/secondary text. */
24
+ textMuted: string;
25
+ /** Border colour. */
26
+ border: string;
27
+ /** Border for inputs etc. */
28
+ borderInput: string;
29
+ /** Font family (sans). */
30
+ fontSans: string;
31
+ /** Border radius (e.g. 8px, 0.5rem). */
32
+ radius: string;
33
+ /** Error/destructive background. */
34
+ errorBg: string;
35
+ /** Error border. */
36
+ errorBorder: string;
37
+ /** Error text. */
38
+ errorText: string;
39
+ }
40
+
41
+ export const DEFAULT_BOOKING_THEME: BookingTheme = {
42
+ primary: '#059669',
43
+ primaryHover: '#047857',
44
+ bg: '#f5f5f4',
45
+ bgEnd: '#e7e5e4',
46
+ surface: '#ffffff',
47
+ headerBg: '#1c1917',
48
+ headerText: '#ffffff',
49
+ text: '#1c1917',
50
+ textMuted: '#78716c',
51
+ border: '#e7e5e4',
52
+ borderInput: '#d6d3d1',
53
+ fontSans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
54
+ radius: '1rem',
55
+ errorBg: '#fef2f2',
56
+ errorBorder: '#fecaca',
57
+ errorText: '#b91c1c',
58
+ };
59
+
60
+ const THEME_VAR_KEYS: (keyof BookingTheme)[] = [
61
+ 'primary', 'primaryHover', 'bg', 'bgEnd', 'surface', 'headerBg', 'headerText',
62
+ 'text', 'textMuted', 'border', 'borderInput', 'fontSans', 'radius',
63
+ 'errorBg', 'errorBorder', 'errorText',
64
+ ];
65
+
66
+ /** Convert theme to React style object for CSS variables (--booking-*). */
67
+ export function themeToCssVars(theme: BookingTheme): Record<string, string> {
68
+ const style: Record<string, string> = {};
69
+ for (const key of THEME_VAR_KEYS) {
70
+ const value = theme[key];
71
+ if (value != null) {
72
+ const varName = '--booking-' + key.replace(/([A-Z])/g, (m) => '-' + m.toLowerCase());
73
+ style[varName] = value;
74
+ }
75
+ }
76
+ return style;
77
+ }
78
+
79
+ /** Merge partial theme with defaults. */
80
+ export function mergeTheme(partial?: Partial<BookingTheme>): BookingTheme {
81
+ if (!partial) return DEFAULT_BOOKING_THEME;
82
+ return { ...DEFAULT_BOOKING_THEME, ...partial };
83
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * W3C Trace Context for TicketBooth: stable trace id per tab + new span per outbound request/retry.
3
+ * Keep storage keys in sync with `packages/viavia-booking/src/trace-context.ts`.
4
+ */
5
+
6
+ import {
7
+ BOOKING_CORRELATION_HEADER,
8
+ getOrCreateBookingCorrelationId,
9
+ } from './correlation-id';
10
+
11
+ export const BOOKING_TRACEPARENT_HEADER = 'traceparent';
12
+
13
+ const STORAGE_W3C_TRACE_ID = 'tb_booking_w3c_trace_id';
14
+
15
+ function randomHex(byteLength: number): string {
16
+ const buf = new Uint8Array(byteLength);
17
+ crypto.getRandomValues(buf);
18
+ return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('');
19
+ }
20
+
21
+ function getOrCreateW3CTraceId(): string {
22
+ if (typeof window === 'undefined') {
23
+ return randomHex(16);
24
+ }
25
+ try {
26
+ let tid = sessionStorage.getItem(STORAGE_W3C_TRACE_ID);
27
+ if (!tid || !/^[0-9a-f]{32}$/i.test(tid)) {
28
+ tid = randomHex(16);
29
+ sessionStorage.setItem(STORAGE_W3C_TRACE_ID, tid);
30
+ }
31
+ return tid.toLowerCase();
32
+ } catch {
33
+ return randomHex(16);
34
+ }
35
+ }
36
+
37
+ /** New span per call (each retry should invoke this again via `withBookingOutboundHeaders`). */
38
+ export function buildTraceparent(): string {
39
+ const traceId = getOrCreateW3CTraceId();
40
+ const spanId = randomHex(8);
41
+ return `00-${traceId}-${spanId}-01`;
42
+ }
43
+
44
+ export function traceIdFromTraceparent(traceparent: string): string | undefined {
45
+ const parts = traceparent.trim().split('-');
46
+ if (parts.length < 4) return undefined;
47
+ const tid = parts[1];
48
+ if (tid.length !== 32 || !/^[0-9a-fA-F]+$/.test(tid)) return undefined;
49
+ return tid.toLowerCase();
50
+ }
51
+
52
+ /** Adds `X-Correlation-Id` and W3C `traceparent` (fresh span each call). */
53
+ export function withBookingOutboundHeaders(
54
+ headers: Record<string, string>,
55
+ ): Record<string, string> {
56
+ if (typeof window === 'undefined') return headers;
57
+ const cid = getOrCreateBookingCorrelationId();
58
+ const next = { ...headers };
59
+ if (cid) next[BOOKING_CORRELATION_HEADER] = cid;
60
+ next[BOOKING_TRACEPARENT_HEADER] = buildTraceparent();
61
+ return next;
62
+ }
@@ -0,0 +1,9 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * Utility function to merge Tailwind CSS classes with clsx
6
+ */
7
+ export function cn(...inputs: ClassValue[]) {
8
+ return twMerge(clsx(inputs));
9
+ }