@coras-io/embed 0.2.0 → 0.4.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 (237) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +8 -7
  3. package/dist/components/accordion/accordion-item/accordion-item.styles.js +1 -1
  4. package/dist/components/alert/alert.styles.js +14 -14
  5. package/dist/components/attraction-card/base/attraction-card.styles.js +7 -3
  6. package/dist/components/attraction-discovery/attraction-discovery.styles.js +1 -1
  7. package/dist/components/attraction-header/attraction-header.styles.js +1 -1
  8. package/dist/components/attraction-option/attraction-option.styles.js +4 -4
  9. package/dist/components/basket/basket.styles.js +27 -26
  10. package/dist/components/basket-empty/basket-empty.styles.js +1 -0
  11. package/dist/components/basket-item/basket-item.component.js +10 -4
  12. package/dist/components/basket-item/basket-item.styles.js +9 -3
  13. package/dist/components/basket-item-ticket/basket-item-ticket.styles.js +1 -3
  14. package/dist/components/basket-mobile/basket-mobile.styles.js +5 -5
  15. package/dist/components/better-carousel/better-carousel.styles.js +2 -0
  16. package/dist/components/bottom-drawer/bottom-drawer.styles.js +6 -4
  17. package/dist/components/button/button.component.d.ts +0 -1
  18. package/dist/components/button/button.component.js +1 -8
  19. package/dist/components/button/button.constants.d.ts +1 -1
  20. package/dist/components/button/button.constants.js +1 -1
  21. package/dist/components/button/button.styles.js +66 -39
  22. package/dist/components/calendar/base/calendar.styles.js +3 -5
  23. package/dist/components/calendar/multi/multi.styles.js +7 -0
  24. package/dist/components/category-tag/category-tag.styles.js +10 -7
  25. package/dist/components/checkbox/checkbox.styles.js +2 -2
  26. package/dist/components/chip/chip.styles.js +2 -2
  27. package/dist/components/collect-fida/collect-fida.component.js +5 -3
  28. package/dist/components/collect-fida/collect-fida.styles.js +9 -16
  29. package/dist/components/crypto-auth-modal/crypto-auth-modal.component.js +6 -4
  30. package/dist/components/crypto-auth-modal/crypto-auth-modal.styles.js +2 -2
  31. package/dist/components/dialog/dialog.styles.js +10 -17
  32. package/dist/components/drawer/drawer.component.js +6 -0
  33. package/dist/components/drawer/drawer.styles.js +17 -3
  34. package/dist/components/dropdown/dropdown.component.js +7 -0
  35. package/dist/components/dropdown/dropdown.styles.js +3 -6
  36. package/dist/components/expiration-timer/expiration-timer.styles.js +3 -3
  37. package/dist/components/explore-card/explore-card.styles.js +1 -1
  38. package/dist/components/filter-chip/filter-chip.styles.js +11 -9
  39. package/dist/components/filters-drawer/filters-drawer.component.js +1 -1
  40. package/dist/components/filters-drawer/filters-drawer.styles.js +6 -5
  41. package/dist/components/image/image.styles.js +3 -3
  42. package/dist/components/image-chip/image-chip.styles.js +1 -1
  43. package/dist/components/input-field/input-field.styles.js +22 -17
  44. package/dist/components/itinerary/itinerary-item/itinerary-item.styles.js +1 -1
  45. package/dist/components/joined-fields/joined-fields.styles.js +10 -6
  46. package/dist/components/loading-spinner/loading-spinner.styles.js +1 -1
  47. package/dist/components/location-image/location-image.component.d.ts +6 -0
  48. package/dist/components/location-image/location-image.component.js +15 -2
  49. package/dist/components/location-image/location-image.styles.js +1 -1
  50. package/dist/components/locations-results/locations-results.styles.js +4 -6
  51. package/dist/components/marketing-tag/marketing-tag.styles.js +1 -1
  52. package/dist/components/message-card/message-card.styles.js +6 -6
  53. package/dist/components/option-group/option-chip/option-chip.styles.js +4 -3
  54. package/dist/components/order-confirmation/order-confirmation-item/order-confirmation-item.styles.js +9 -0
  55. package/dist/components/order-confirmation/order-confirmation.component.js +4 -3
  56. package/dist/components/order-confirmation/order-confirmation.styles.js +1 -1
  57. package/dist/components/otp/otp.styles.js +6 -6
  58. package/dist/components/overlay/overlay.styles.js +5 -1
  59. package/dist/components/pay-in/pay-in.styles.js +2 -2
  60. package/dist/components/payment-fiat-method/payment-fiat-method.styles.js +20 -4
  61. package/dist/components/payment-option/payment-option.styles.js +10 -12
  62. package/dist/components/performance-time/performance-time.styles.js +2 -2
  63. package/dist/components/phone-country-code/phone-country-code.styles.js +1 -1
  64. package/dist/components/points/points.styles.js +1 -1
  65. package/dist/components/search-button/search-button.styles.js +15 -7
  66. package/dist/components/seated-selector/seated-selector.component.d.ts +2 -0
  67. package/dist/components/seated-selector/seated-selector.component.js +53 -10
  68. package/dist/components/seated-selector/seated-selector.styles.js +77 -14
  69. package/dist/components/seats-io/seats-io.component.d.ts +2 -0
  70. package/dist/components/seats-io/seats-io.component.js +25 -2
  71. package/dist/components/seats-io/seats-io.constants.d.ts +2 -1
  72. package/dist/components/seats-io/seats-io.constants.js +6 -1
  73. package/dist/components/seats-io/seats-io.transform.d.ts +8 -2
  74. package/dist/components/seats-io/seats-io.transform.js +12 -4
  75. package/dist/components/select-list/select-list.styles.js +1 -0
  76. package/dist/components/select-list/select-option/select-option.styles.js +3 -3
  77. package/dist/components/sort-dropdown/sort-dropdown.styles.js +1 -1
  78. package/dist/components/sort-list/sort-list.styles.js +1 -1
  79. package/dist/components/sport-options/sport-option/sport-option.styles.js +2 -2
  80. package/dist/components/star-rating/star-rating.component.d.ts +1 -0
  81. package/dist/components/star-rating/star-rating.component.js +19 -23
  82. package/dist/components/star-rating/star-rating.styles.js +32 -6
  83. package/dist/components/tab/tab.styles.js +1 -1
  84. package/dist/components/tabs/tabs.styles.js +1 -1
  85. package/dist/components/tag/tag.styles.js +7 -7
  86. package/dist/components/text-section/text-section.styles.js +2 -1
  87. package/dist/components/time-tag/time-tag.styles.js +1 -2
  88. package/dist/components/tooltip/tooltip.styles.js +3 -3
  89. package/dist/components/tour-ticket-type-selector/tour-ticket-type/tour-ticket-type.styles.js +2 -2
  90. package/dist/components/tour-ticket-type-selector/tour-ticket-type-selector.component.js +1 -1
  91. package/dist/components/tour-ticket-type-selector/tour-ticket-type-selector.styles.js +1 -10
  92. package/dist/components/transfer-modal/transfer-modal.component.js +3 -3
  93. package/dist/components/transfer-modal/transfer-modal.styles.js +6 -8
  94. package/dist/containers/basket/basket.component.d.ts +1 -1
  95. package/dist/containers/basket/basket.component.js +56 -25
  96. package/dist/containers/footer/footer.component.d.ts +2 -0
  97. package/dist/containers/footer/footer.component.js +8 -0
  98. package/dist/containers/footer/footer.styles.js +12 -0
  99. package/dist/containers/navbar/navbar.component.d.ts +6 -1
  100. package/dist/containers/navbar/navbar.component.js +41 -9
  101. package/dist/containers/navbar/navbar.styles.js +47 -13
  102. package/dist/contexts/basket/basket.provider.d.ts +1 -0
  103. package/dist/contexts/basket/basket.provider.js +31 -4
  104. package/dist/contexts/properties.context.d.ts +4 -4
  105. package/dist/core/api/middlewares/booking-session/booking-session.constants.d.ts +1 -0
  106. package/dist/core/api/middlewares/booking-session/booking-session.constants.js +1 -0
  107. package/dist/core/api/middlewares/booking-session/booking-session.js +18 -27
  108. package/dist/core/api/middlewares/booking-session/booking-session.utils.d.ts +4 -4
  109. package/dist/core/api/middlewares/booking-session/booking-session.utils.js +13 -10
  110. package/dist/core/api/queries.js +3 -2
  111. package/dist/core/color-scheme.d.ts +14 -0
  112. package/dist/core/color-scheme.js +48 -0
  113. package/dist/core/currency/constants.d.ts +1 -1
  114. package/dist/core/currency/constants.js +4 -3
  115. package/dist/core/i18n/date.js +20 -9
  116. package/dist/core/i18n/locale-codes.d.ts +2 -2
  117. package/dist/core/i18n/locale-codes.js +4 -0
  118. package/dist/core/images/constants.d.ts +0 -5
  119. package/dist/core/images/constants.js +0 -5
  120. package/dist/core/images/helpers.d.ts +8 -0
  121. package/dist/core/images/helpers.js +14 -6
  122. package/dist/core/images/srcset.js +2 -2
  123. package/dist/core/storybook/demo.d.ts +4 -0
  124. package/dist/core/storybook/demo.js +17 -0
  125. package/dist/core/types/locales.d.ts +7 -0
  126. package/dist/core/utils/datetime.js +10 -4
  127. package/dist/core/utils/locale.d.ts +8 -1
  128. package/dist/core/utils/locale.js +10 -0
  129. package/dist/helpers/helpers.d.ts +1 -1
  130. package/dist/index.d.ts +2 -3
  131. package/dist/index.js +1 -2
  132. package/dist/locales/ar-SA.d.ts +279 -0
  133. package/dist/locales/ar-SA.js +288 -0
  134. package/dist/locales/da-DK.d.ts +100 -82
  135. package/dist/locales/da-DK.js +104 -89
  136. package/dist/locales/de-DE.d.ts +100 -82
  137. package/dist/locales/de-DE.js +104 -89
  138. package/dist/locales/en-IE.d.ts +250 -232
  139. package/dist/locales/en-IE.js +254 -239
  140. package/dist/locales/en-US.d.ts +100 -82
  141. package/dist/locales/en-US.js +107 -92
  142. package/dist/locales/es-ES.d.ts +100 -82
  143. package/dist/locales/es-ES.js +104 -89
  144. package/dist/locales/fr-FR.d.ts +100 -82
  145. package/dist/locales/fr-FR.js +104 -89
  146. package/dist/locales/fr-MA.d.ts +100 -82
  147. package/dist/locales/fr-MA.js +104 -89
  148. package/dist/locales/is-IS.d.ts +100 -82
  149. package/dist/locales/is-IS.js +104 -89
  150. package/dist/locales/it-IT.d.ts +100 -82
  151. package/dist/locales/it-IT.js +104 -89
  152. package/dist/locales/ms-MY.d.ts +100 -82
  153. package/dist/locales/ms-MY.js +106 -91
  154. package/dist/locales/nb-NO.d.ts +100 -82
  155. package/dist/locales/nb-NO.js +104 -89
  156. package/dist/locales/pl-PL.d.ts +100 -82
  157. package/dist/locales/pl-PL.js +104 -89
  158. package/dist/locales/pt-PT.d.ts +100 -82
  159. package/dist/locales/pt-PT.js +104 -89
  160. package/dist/locales/sv-SE.d.ts +100 -82
  161. package/dist/locales/sv-SE.js +104 -89
  162. package/dist/locales/th-TH.d.ts +100 -82
  163. package/dist/locales/th-TH.js +104 -89
  164. package/dist/locales/tr-TR.d.ts +100 -82
  165. package/dist/locales/tr-TR.js +104 -89
  166. package/dist/locales/zh-CN.d.ts +100 -82
  167. package/dist/locales/zh-CN.js +105 -90
  168. package/dist/locales/zh-HK.d.ts +279 -0
  169. package/dist/locales/zh-HK.js +288 -0
  170. package/dist/mixins/coinbase.mixin.js +1 -1
  171. package/dist/mixins/error-modal.styles.js +2 -2
  172. package/dist/mixins/location-search/location-search.js +3 -3
  173. package/dist/mixins/location-search/location-search.styles.js +6 -5
  174. package/dist/sdk/chrome.d.ts +9 -3
  175. package/dist/sdk/chrome.js +21 -9
  176. package/dist/sdk/element-adapter.js +11 -8
  177. package/dist/sdk/mount.d.ts +3 -3
  178. package/dist/sdk/mount.js +153 -48
  179. package/dist/sdk/navigation-intent.js +3 -0
  180. package/dist/sdk/page-registry.d.ts +14 -2
  181. package/dist/sdk/page-registry.js +33 -13
  182. package/dist/sdk/theme.d.ts +2 -16
  183. package/dist/sdk/theme.js +12 -44
  184. package/dist/sdk/types.d.ts +59 -30
  185. package/dist/sdk/url.d.ts +24 -3
  186. package/dist/sdk/url.js +66 -27
  187. package/dist/sdk/validation.d.ts +0 -7
  188. package/dist/sdk/validation.js +104 -48
  189. package/dist/styles/contrast-fixtures.d.ts +0 -5
  190. package/dist/styles/contrast-fixtures.js +79 -9
  191. package/dist/styles/contrast.js +4 -3
  192. package/dist/styles/defaults/component.js +17 -1
  193. package/dist/styles/primitives.d.ts +0 -10
  194. package/dist/styles/primitives.js +2 -12
  195. package/dist/styles/tokens.d.ts +1 -1
  196. package/dist/styles/tokens.generated.d.ts +1 -1
  197. package/dist/styles/tokens.generated.js +1 -1
  198. package/dist/styles/tokens.metadata.d.ts +1 -1
  199. package/dist/styles/tokens.metadata.js +378 -90
  200. package/dist/views/details-page/details-page.component.d.ts +5 -5
  201. package/dist/views/details-page/details-page.component.js +77 -80
  202. package/dist/views/details-page/details-page.styles.js +64 -9
  203. package/dist/views/help-page/help-page.component.d.ts +1 -0
  204. package/dist/views/help-page/help-page.component.js +11 -4
  205. package/dist/views/help-page/help-page.styles.js +21 -8
  206. package/dist/views/landing-page/landing-page.component.d.ts +4 -4
  207. package/dist/views/landing-page/landing-page.component.js +65 -65
  208. package/dist/views/landing-page/landing-page.styles.js +70 -3
  209. package/dist/views/payment-page/partials/activity-details/activity-details.styles.js +10 -33
  210. package/dist/views/payment-page/partials/billing-address/billing-address.styles.js +4 -9
  211. package/dist/views/payment-page/partials/confirmation/confirmation.styles.js +6 -6
  212. package/dist/views/payment-page/partials/hotel-pickup-list/hotel-pickup-list.styles.js +4 -4
  213. package/dist/views/payment-page/partials/payment-type-selector/payment-type-selector.styles.js +1 -0
  214. package/dist/views/payment-page/partials/stripe/stripe.component.d.ts +1 -1
  215. package/dist/views/payment-page/partials/stripe/stripe.component.js +11 -12
  216. package/dist/views/payment-page/partials/stripe/stripe.constants.d.ts +2 -15
  217. package/dist/views/payment-page/partials/stripe/stripe.constants.js +4 -17
  218. package/dist/views/payment-page/partials/stripe/stripe.styles.js +16 -23
  219. package/dist/views/payment-page/partials/stripe/stripe.utils.d.ts +25 -14
  220. package/dist/views/payment-page/partials/stripe/stripe.utils.js +111 -62
  221. package/dist/views/payment-page/partials/terms-checkbox/terms-checkbox.styles.js +1 -1
  222. package/dist/views/payment-page/payment-page.component.d.ts +1 -0
  223. package/dist/views/payment-page/payment-page.component.js +19 -9
  224. package/dist/views/payment-page/payment-page.styles.js +37 -32
  225. package/dist/views/payment-page/payment-page.utils.d.ts +7 -0
  226. package/dist/views/payment-page/payment-page.utils.js +29 -14
  227. package/dist/views/payment-page/payment-page.validators.js +9 -3
  228. package/dist/views/payment-page/services/payment-api.service.d.ts +1 -1
  229. package/dist/views/search-page/search-page.component.d.ts +1 -0
  230. package/dist/views/search-page/search-page.component.js +12 -6
  231. package/dist/views/search-page/search-page.styles.js +24 -2
  232. package/dist/views/suggestion-widget/suggestion-widget.styles.js +1 -1
  233. package/package.json +63 -76
  234. package/dist/custom-elements.json +0 -37397
  235. package/dist/sdk/meta.d.ts +0 -7
  236. package/dist/sdk/meta.js +0 -9
  237. package/dist/styles/tokens.css +0 -359
@@ -1,4 +1,4 @@
1
- import type { BrandConfig } from "@coras-io/brand-tokens";
1
+ import type { BrandConfig, BrandJson } from "@coras-io/brand-tokens";
2
2
  import type { SupportedCurrencies } from "../core/types/core/currency.js";
3
3
  import type { SupportedLocales } from "../core/types/core/locale.js";
4
4
  /**
@@ -57,12 +57,10 @@ export type CorasPageParamsByPage = {
57
57
  /** Params for a specific page, or the union when the page is not narrowed. */
58
58
  export type CorasPageParams<P extends CorasPageName = CorasPageName> = CorasPageParamsByPage[P];
59
59
  /**
60
- * The SDK `theme` option is the shared `BrandConfig` contract: un-prefixed
61
- * semantic keys (`primary`, `secondary`, ) plus chrome, `logo`, and
62
- * `fontFaces`. Defined once in `@coras-io/brand-tokens` and reused by the
63
- * microsites, emails, and the Studio.
60
+ * The SDK `theme` option is the shared brand contract from `@coras-io/brand-tokens`:
61
+ * a `brand.json` as written (grouped by surface) or the engine's flat keys.
64
62
  */
65
- export type CorasTheme = BrandConfig;
63
+ export type CorasTheme = BrandConfig | BrandJson;
66
64
  /**
67
65
  * Which colour scheme the mount renders. `theme` carries the light values and
68
66
  * its `dark` block the dark overrides; this picks which side the `light-dark()`
@@ -70,11 +68,12 @@ export type CorasTheme = BrandConfig;
70
68
  *
71
69
  * - `"light"` (default) - always light, ignoring the host page.
72
70
  * - `"dark"` - always dark.
73
- * - `"system"` - follows the OS via `prefers-color-scheme`.
74
71
  * - `"inherit"` - follows the host page's own `color-scheme` (the SDK sets none),
75
- * so the embed behaves like a native element on a light or dark site.
72
+ * so the embed behaves like a native element on a light or dark site. A page
73
+ * that declares `color-scheme: light dark` hands the embed the visitor's OS
74
+ * preference through the same route.
76
75
  */
77
- export type CorasColorScheme = "light" | "dark" | "system" | "inherit";
76
+ export type CorasColorScheme = "light" | "dark" | "inherit";
78
77
  export type CorasConfig = {
79
78
  apiUrl: string;
80
79
  distributorId: string;
@@ -157,21 +156,33 @@ export type CorasNavigationSource = "embed" | "host";
157
156
  export type CorasReadyDetail = {
158
157
  page: CorasPageName;
159
158
  };
160
- export type CorasNavigateDetail = {
161
- page: CorasPageName;
162
- params?: CorasPageParams;
159
+ type NavigateDetailFor<P extends CorasPageName> = {
160
+ page: P;
161
+ params?: CorasPageParamsByPage[P];
163
162
  locale?: SupportedLocales;
164
163
  currency?: SupportedCurrencies;
165
164
  href?: string;
166
165
  source: CorasNavigationSource;
167
166
  };
168
- export type CorasStateChangeDetail = {
169
- page: CorasPageName;
170
- params: CorasPageParams;
167
+ /**
168
+ * Distributes over the page union, so narrowing on `detail.page` narrows
169
+ * `detail.params` with it and a handler cannot read a param the page never has.
170
+ */
171
+ export type CorasNavigateDetail<P extends CorasPageName = CorasPageName> = P extends CorasPageName ? NavigateDetailFor<P> : never;
172
+ type StateChangeDetailFor<P extends CorasPageName> = {
173
+ page: P;
174
+ params: CorasPageParamsByPage[P];
171
175
  locale?: SupportedLocales;
172
176
  currency?: SupportedCurrencies;
173
177
  };
174
- export type CorasPaymentStatus = "pending" | "succeeded" | "failed" | "cancelled";
178
+ /** Distributes over the page union (see `CorasNavigateDetail`). */
179
+ export type CorasStateChangeDetail<P extends CorasPageName = CorasPageName> = P extends CorasPageName ? StateChangeDetailFor<P> : never;
180
+ /**
181
+ * Terminal payment outcomes the SDK emits. Only settled results are surfaced:
182
+ * a `pending` in-flight state is internal, and a user cancellation returns the
183
+ * form to its editable state rather than emitting a distinct status.
184
+ */
185
+ export type CorasPaymentStatus = "succeeded" | "failed";
175
186
  export type CorasPaymentStatusDetail = {
176
187
  status: CorasPaymentStatus;
177
188
  /** Public reference only; never provider secrets or card data. */
@@ -208,6 +219,16 @@ export type CorasCallbacks = {
208
219
  onError?: (detail: CorasErrorDetail) => void;
209
220
  onObservability?: (event: CorasObservabilityEvent) => void;
210
221
  };
222
+ /**
223
+ * `mount()` is generic over `P`, so a call passing a literal page gets its own
224
+ * params checked against it. The alias is deliberately *not* distributed over
225
+ * the page union: a host that owns routing reads `page` and `params` out of a
226
+ * URL as separate values, and no annotation can prove that pair correlates.
227
+ * Distributing here would force every routed host to cast - the SDK's primary
228
+ * integration - to buy a check the call site already makes. The read-side types
229
+ * (`CorasState`, `CorasNavigateDetail`, `CorasStateChangeDetail`) do distribute,
230
+ * because narrowing there is free.
231
+ */
211
232
  export type CorasMountOptions<P extends CorasPageName = CorasPageName> = {
212
233
  container: HTMLElement;
213
234
  page: P;
@@ -226,17 +247,29 @@ export type CorasMountOptions<P extends CorasPageName = CorasPageName> = {
226
247
  */
227
248
  preview?: boolean;
228
249
  } & CorasCallbacks;
229
- /** Frozen snapshot of the mount's current public state. */
230
- export type CorasState = {
231
- readonly page: CorasPageName;
232
- readonly params: CorasPageParams;
250
+ type StateFor<P extends CorasPageName> = {
251
+ readonly page: P;
252
+ readonly params: CorasPageParamsByPage[P];
233
253
  readonly locale: SupportedLocales;
234
254
  readonly currency: SupportedCurrencies;
235
255
  };
236
- export type CorasUpdate = {
237
- page?: CorasPageName;
238
- params?: CorasPageParams;
239
- config?: Partial<CorasConfig>;
256
+ /**
257
+ * Frozen snapshot of the mount's current public state. Distributes over the
258
+ * page union, so `state.page` narrows `state.params` with it.
259
+ */
260
+ export type CorasState<P extends CorasPageName = CorasPageName> = P extends CorasPageName ? StateFor<P> : never;
261
+ /**
262
+ * Config an update may change. `apiUrl` and `distributorId` are the mount's
263
+ * fixed identity (the API client is built once around them), so they are not
264
+ * updatable; unmount and mount again to point at a different endpoint or
265
+ * distributor.
266
+ */
267
+ export type CorasConfigUpdate = Partial<Omit<CorasConfig, "apiUrl" | "distributorId">>;
268
+ /** Generic over `P`, not distributed - see `CorasMountOptions` for why. */
269
+ export type CorasUpdate<P extends CorasPageName = CorasPageName> = {
270
+ page?: P;
271
+ params?: CorasPageParamsByPage[P];
272
+ config?: CorasConfigUpdate;
240
273
  chrome?: CorasChrome;
241
274
  };
242
275
  export type CorasNavigateOptions = {
@@ -246,13 +279,9 @@ export type CorasNavigateOptions = {
246
279
  export type CorasApp = {
247
280
  readonly ready: Promise<void>;
248
281
  readonly state: CorasState;
249
- update(patch: CorasUpdate): void;
282
+ update<P extends CorasPageName = CorasPageName>(patch: CorasUpdate<P>): void;
250
283
  navigate<P extends CorasPageName>(page: P, params?: CorasPageParamsByPage[P], options?: CorasNavigateOptions): void;
251
284
  prefetch(page: CorasPageName): Promise<void>;
252
285
  unmount(): void;
253
286
  };
254
- export type CorasPageMeta = {
255
- page: CorasPageName;
256
- /** Internal custom-element tag used to render the page. */
257
- tagName: string;
258
- };
287
+ export {};
package/dist/sdk/url.d.ts CHANGED
@@ -6,6 +6,23 @@ import type { CorasPageName, CorasPageParams } from "./types.js";
6
6
  * URL; these helpers translate between a Coras state and a canonical URL so each
7
7
  * framework does not write bespoke parsing glue. See ADR 0013.
8
8
  */
9
+ /**
10
+ * Pages with a URL. `suggestion-widget` is absent by design: it is a standalone
11
+ * embed the host mounts directly, not a page a visitor navigates to.
12
+ */
13
+ export type CorasRoutablePage = Exclude<CorasPageName, "suggestion-widget">;
14
+ /**
15
+ * A URL-representable state.
16
+ *
17
+ * `page` and `params` are deliberately left uncorrelated, and `page` stays the
18
+ * full union: a host that owns routing feeds these helpers straight from a
19
+ * `coras:navigate` detail or its own router, where the pair arrives as separate
20
+ * values no annotation can prove match. Narrowing here would buy nothing the
21
+ * runtime does not already check and would force every routed host to
22
+ * destructure per page. `buildCorasUrl` enforces the real constraints - a
23
+ * routable page, a details id, and an id that clears the reserved segments -
24
+ * and throws rather than emit a URL that parses back as something else.
25
+ */
9
26
  export type CorasUrlState = {
10
27
  page: CorasPageName;
11
28
  params: CorasPageParams;
@@ -23,12 +40,16 @@ export type CorasUrlStrategy = {
23
40
  currencyParam?: string;
24
41
  /**
25
42
  * Path segment for pages that use a reserved keyword. `landing` has no
26
- * segment and `details` is represented by a bare id segment.
43
+ * segment and `details` is represented by a bare, percent-encoded id segment.
44
+ * Reserved segments therefore carve their words out of the details id space:
45
+ * an id equal to one is rejected by `buildCorasUrl` rather than silently
46
+ * building a URL that parses back as another page. Rename them here if they
47
+ * collide with real ids.
27
48
  */
28
- reservedSegments?: Partial<Record<CorasPageName, string>>;
49
+ reservedSegments?: Partial<Record<CorasRoutablePage, string>>;
29
50
  };
30
51
  export declare const DEFAULT_URL_STRATEGY: Required<Omit<CorasUrlStrategy, "reservedSegments">> & {
31
- reservedSegments: Partial<Record<CorasPageName, string>>;
52
+ reservedSegments: Partial<Record<CorasRoutablePage, string>>;
32
53
  };
33
54
  /** Parse a (relative or absolute) URL into a Coras state. */
34
55
  export declare function parseCorasUrl(url: string, strategy?: CorasUrlStrategy): CorasUrlState;
package/dist/sdk/url.js CHANGED
@@ -1,9 +1,8 @@
1
- import { normalizeCurrency, normalizeLocale, PAGE_PARAM_KEYS, PAGE_PARAM_TYPES, } from "./validation.js";
1
+ import { CorasValidationError, normalizeCurrency, normalizeLocale, PAGE_PARAM_KEYS, } from "./validation.js";
2
2
  const DEFAULT_RESERVED = {
3
3
  search: "search",
4
4
  payment: "payment",
5
5
  help: "help",
6
- "suggestion-widget": "widget",
7
6
  };
8
7
  export const DEFAULT_URL_STRATEGY = {
9
8
  basePath: "",
@@ -12,14 +11,33 @@ export const DEFAULT_URL_STRATEGY = {
12
11
  currencyParam: "currency",
13
12
  reservedSegments: DEFAULT_RESERVED,
14
13
  };
14
+ function invalidUrlState(message, field) {
15
+ throw new CorasValidationError("invalid_page_params", message, field);
16
+ }
15
17
  function resolveStrategy(strategy = {}) {
18
+ const reservedSegments = {
19
+ ...DEFAULT_RESERVED,
20
+ ...strategy.reservedSegments,
21
+ };
22
+ // A segment holding a `/` (or nothing) would split into a path shape the
23
+ // parser can never invert, and two pages sharing a segment make the parse
24
+ // ambiguous. Both are host configuration errors: fail loudly at the call
25
+ // rather than emit URLs that silently round-trip to the wrong page.
26
+ const seen = new Map();
27
+ for (const [page, segment] of Object.entries(reservedSegments)) {
28
+ if (!segment || segment !== segment.trim() || /[/?#]/.test(segment)) {
29
+ invalidUrlState(`reservedSegments.${page} must be a non-empty path segment without "/", "?" or "#".`, `reservedSegments.${page}`);
30
+ }
31
+ const owner = seen.get(segment);
32
+ if (owner) {
33
+ invalidUrlState(`reservedSegments."${segment}" is used by both "${owner}" and "${page}"; each page needs its own segment.`, `reservedSegments.${page}`);
34
+ }
35
+ seen.set(segment, page);
36
+ }
16
37
  return {
17
38
  ...DEFAULT_URL_STRATEGY,
18
39
  ...strategy,
19
- reservedSegments: {
20
- ...DEFAULT_RESERVED,
21
- ...strategy.reservedSegments,
22
- },
40
+ reservedSegments,
23
41
  };
24
42
  }
25
43
  function reservedToPage(reserved) {
@@ -33,22 +51,15 @@ function reservedToPage(reserved) {
33
51
  function trimSlashes(value) {
34
52
  return value.replace(/^\/+|\/+$/g, "");
35
53
  }
36
- // Decode a query value into the type `mount()` validation expects. An
37
- // unparseable value is left as the raw string so validation reports it instead
38
- // of the helper silently swallowing it.
39
- function decodeParam(key, value) {
40
- const type = PAGE_PARAM_TYPES[key];
41
- if (type === "number") {
42
- const n = Number(value);
43
- return value.trim() !== "" && Number.isFinite(n) ? n : value;
44
- }
45
- if (type === "boolean") {
46
- if (value === "true")
47
- return true;
48
- if (value === "false")
49
- return false;
54
+ // Decode a percent-encoded path segment, falling back to the raw value when it
55
+ // holds a malformed escape (so a hand-written URL is never a hard failure).
56
+ function safeDecodeSegment(value) {
57
+ try {
58
+ return decodeURIComponent(value);
59
+ }
60
+ catch {
61
+ return value;
50
62
  }
51
- return value;
52
63
  }
53
64
  /** Parse a (relative or absolute) URL into a Coras state. */
54
65
  export function parseCorasUrl(url, strategy = {}) {
@@ -89,6 +100,7 @@ export function parseCorasUrl(url, strategy = {}) {
89
100
  }
90
101
  const reservedMap = reservedToPage(resolved.reservedSegments);
91
102
  const pageSegment = segments[index];
103
+ // Only ever `landing`, a reserved page, or `details` - all routable.
92
104
  let page = "landing";
93
105
  const params = {};
94
106
  if (pageSegment) {
@@ -97,8 +109,9 @@ export function parseCorasUrl(url, strategy = {}) {
97
109
  page = reservedPage;
98
110
  }
99
111
  else {
112
+ // Any non-reserved segment is a (percent-encoded) details id.
100
113
  page = "details";
101
- params.id = pageSegment;
114
+ params.id = safeDecodeSegment(pageSegment);
102
115
  }
103
116
  }
104
117
  // Page-specific params come from the query string.
@@ -108,13 +121,18 @@ export function parseCorasUrl(url, strategy = {}) {
108
121
  continue; // details id is the path segment
109
122
  if (key === "metadata")
110
123
  continue; // host context, not URL state
124
+ // Every param a routable page accepts is a string, so the raw query value
125
+ // is already the type `mount()` validation expects.
111
126
  const value = parsed.searchParams.get(key);
112
127
  if (value != null)
113
- params[key] = decodeParam(key, value);
128
+ params[key] = value;
114
129
  }
130
+ // `page` and `params` are built together above (a details page always got its
131
+ // id from the path segment), so the pairing holds even though the locals are
132
+ // separate values the compiler cannot correlate.
115
133
  return {
116
134
  page,
117
- params: params,
135
+ params,
118
136
  ...(locale ? { locale } : {}),
119
137
  ...(currency ? { currency } : {}),
120
138
  };
@@ -122,6 +140,11 @@ export function parseCorasUrl(url, strategy = {}) {
122
140
  /** Build a canonical URL from a Coras state. */
123
141
  export function buildCorasUrl(state, strategy = {}) {
124
142
  const resolved = resolveStrategy(strategy);
143
+ // The type already excludes it; this catches JavaScript hosts, for whom a
144
+ // silent landing URL would be the alternative.
145
+ if (state.page === "suggestion-widget") {
146
+ invalidUrlState("suggestion-widget has no URL: it is a standalone embed the host mounts directly, not a routable page.", "page");
147
+ }
125
148
  const segments = [];
126
149
  const base = trimSlashes(resolved.basePath);
127
150
  if (base)
@@ -132,8 +155,22 @@ export function buildCorasUrl(state, strategy = {}) {
132
155
  segments.push(state.currency);
133
156
  const params = (state.params ?? {});
134
157
  if (state.page === "details") {
135
- if (params.id != null)
136
- segments.push(String(params.id));
158
+ const id = params.id;
159
+ // The id *is* the route segment, so a details state without one has no URL.
160
+ // Building the bare landing path instead would lose the page silently.
161
+ if (typeof id !== "string" || id.length === 0) {
162
+ invalidUrlState("A details state needs a non-empty params.id: the id is its URL segment.", "params.id");
163
+ }
164
+ // A bare id segment shares its space with the reserved words, so an id that
165
+ // equals one would parse back as that page. Reject it rather than emit a
166
+ // URL that does not round-trip; rename the segment via `reservedSegments`
167
+ // if a real id collides.
168
+ const collision = Object.entries(resolved.reservedSegments).find(([, segment]) => segment === id);
169
+ if (collision) {
170
+ invalidUrlState(`A details id of "${id}" collides with the "${collision[0]}" route segment and would parse back as that page. Rename the segment via reservedSegments.`, "params.id");
171
+ }
172
+ // Encode the id so `/`, `?`, and `#` survive the round-trip.
173
+ segments.push(encodeURIComponent(id));
137
174
  }
138
175
  else {
139
176
  const reserved = resolved.reservedSegments[state.page];
@@ -160,9 +197,11 @@ export function buildCorasUrl(state, strategy = {}) {
160
197
  }
161
198
  /** Normalize locale/currency aliases and ensure a params object. */
162
199
  export function normalizeCorasUrlState(input) {
200
+ // Only locale and currency are rewritten, so the page/params pairing the
201
+ // caller supplied carries through untouched.
163
202
  return {
164
203
  page: input.page,
165
- params: (input.params ?? {}),
204
+ params: input.params ?? {},
166
205
  ...(input.locale
167
206
  ? { locale: normalizeLocale(input.locale) ?? input.locale }
168
207
  : {}),
@@ -21,13 +21,6 @@ export declare const PUBLIC_PAGES: readonly CorasPageName[];
21
21
  export declare function assertValidPage(page: unknown): asserts page is CorasPageName;
22
22
  /** Allowed param keys per page (public names). */
23
23
  export declare const PAGE_PARAM_KEYS: Record<CorasPageName, readonly string[]>;
24
- /**
25
- * URL params whose string form decodes to a non-string type. Everything not
26
- * listed stays a string. Used by the URL helpers so a value round-trips through
27
- * a URL as the type `mount()` validation expects (e.g. `limit=3` -> `3`, not
28
- * `"3"`; `showExploreCard=false` -> `false`, not a truthy `"false"`).
29
- */
30
- export declare const PAGE_PARAM_TYPES: Readonly<Record<string, "number" | "boolean">>;
31
24
  /** Normalize locale casing/separators (e.g. `en_ie`, `EN-IE` -> `en-IE`). */
32
25
  export declare function normalizeLocale(value: string): SupportedLocales | undefined;
33
26
  /** Normalize currency casing (e.g. `eur` -> `EUR`). */
@@ -52,15 +52,31 @@ export const PAGE_PARAM_KEYS = {
52
52
  "suggestion-widget": ["limit", "showExploreCard", "country", "city"],
53
53
  };
54
54
  /**
55
- * URL params whose string form decodes to a non-string type. Everything not
56
- * listed stays a string. Used by the URL helpers so a value round-trips through
57
- * a URL as the type `mount()` validation expects (e.g. `limit=3` -> `3`, not
58
- * `"3"`; `showExploreCard=false` -> `false`, not a truthy `"false"`).
55
+ * The value type of every public page param. Param names mean the same thing on
56
+ * every page that accepts them, so one flat table drives both `mount()`
57
+ * validation and the URL helpers' decoding.
59
58
  */
60
- export const PAGE_PARAM_TYPES = {
59
+ const PAGE_PARAM_KINDS = {
60
+ country: "string",
61
+ city: "string",
62
+ startDate: "date",
63
+ endDate: "date",
64
+ date: "date",
65
+ sort: "string",
66
+ duration: "string",
67
+ price: "string",
68
+ categories: "string",
69
+ search: "string",
70
+ id: "string",
71
+ content: "string",
72
+ metadata: "metadata",
61
73
  limit: "number",
62
74
  showExploreCard: "boolean",
63
75
  };
76
+ /** Dates cross the API as plain `yyyy-MM-dd` calendar days. */
77
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
78
+ /** Upper bound for the suggestion widget's `limit`, guarding absurd requests. */
79
+ const MAX_WIDGET_LIMIT = 50;
64
80
  const CONFIG_KEYS = [
65
81
  "apiUrl",
66
82
  "distributorId",
@@ -75,14 +91,14 @@ const CONFIG_KEYS = [
75
91
  ];
76
92
  const localeSet = new Set(allLocales);
77
93
  const currencySet = new Set(supportedCurrencies);
78
- const colorSchemes = new Set([
79
- "light",
80
- "dark",
81
- "system",
82
- "inherit",
83
- ]);
94
+ const colorSchemes = new Set(["light", "dark", "inherit"]);
84
95
  /** Normalize locale casing/separators (e.g. `en_ie`, `EN-IE` -> `en-IE`). */
85
96
  export function normalizeLocale(value) {
97
+ // Guard the type here rather than at each call site: JavaScript hosts reach
98
+ // this with arbitrary values, and an unsupported one is `undefined` (which
99
+ // callers turn into a typed error) rather than a raw TypeError from `.trim`.
100
+ if (typeof value !== "string")
101
+ return undefined;
86
102
  const parts = value.trim().replace(/_/g, "-").split("-");
87
103
  if (parts.length === 0 || !parts[0])
88
104
  return undefined;
@@ -93,6 +109,8 @@ export function normalizeLocale(value) {
93
109
  }
94
110
  /** Normalize currency casing (e.g. `eur` -> `EUR`). */
95
111
  export function normalizeCurrency(value) {
112
+ if (typeof value !== "string")
113
+ return undefined;
96
114
  const canonical = value.trim().toUpperCase();
97
115
  return currencySet.has(canonical)
98
116
  ? canonical
@@ -122,17 +140,15 @@ export function assertSecureUrl(value, field) {
122
140
  return url.href;
123
141
  throw new CorasValidationError("invalid_url", `${field} must use https (http is only allowed for localhost).`, field);
124
142
  }
125
- // Strict hostname: alnum labels separated by dots, optional :port.
126
- // Rejects protocol-relative inputs, paths, and free-form strings.
127
- const BARE_HOSTNAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::\d{1,5})?$/i;
128
143
  /**
129
144
  * Asset URL must be one of:
130
- * - an https URL (http allowed only for localhost),
131
- * - a bare hostname like `assets.example.com[:port]`,
145
+ * - an absolute https URL (http allowed only for localhost),
132
146
  * - a root-relative path like `/assets`.
133
- * Rejected: protocol-relative URLs (`//host/x`), `javascript:` / `data:` URIs,
134
- * traversal-style paths. `assetsUrl` is interpolated straight into image and
135
- * SVG hrefs (see icon.component.ts), so a permissive validator is a foot-gun.
147
+ * Rejected: protocol-relative URLs (`//host/x`), bare hostnames
148
+ * (`assets.example.com` - the browser would resolve it against the host page's
149
+ * origin), `javascript:` / `data:` URIs, and traversal-style paths. `assetsUrl`
150
+ * is interpolated straight into image and SVG hrefs (see icon.component.ts), so
151
+ * a permissive validator is a foot-gun.
136
152
  */
137
153
  function validateAssetsUrl(value) {
138
154
  if (value === "") {
@@ -160,16 +176,15 @@ function validateAssetsUrl(value) {
160
176
  assertSecureUrl(value, "config.assetsUrl");
161
177
  return;
162
178
  }
163
- if (!BARE_HOSTNAME_RE.test(value)) {
164
- throw new CorasValidationError("invalid_url", "config.assetsUrl must be an https URL, a bare hostname, or a root-relative path.", "config.assetsUrl");
165
- }
179
+ throw new CorasValidationError("invalid_url", "config.assetsUrl must be an absolute https URL or a root-relative path.", "config.assetsUrl");
166
180
  }
167
181
  // Field keys plus the BrandConfig meta keys validateBrandConfig understands.
168
182
  // Used only to surface unknown keys in strict mode (the schema itself strips
169
183
  // them silently, which a strict host wants flagged, not swallowed).
170
184
  const THEME_KEYS = [
171
185
  ...FIELDS.map((field) => field.key),
172
- "logo",
186
+ ...FIELDS.map((field) => field.path.split(".")[0]),
187
+ "$schema",
173
188
  "fontFaces",
174
189
  "dark",
175
190
  "schemaVersion",
@@ -198,6 +213,13 @@ function validateTheme(theme, strict) {
198
213
  }
199
214
  }
200
215
  }
216
+ // Guards a `.map()` over host-supplied config: a JavaScript caller passing a
217
+ // bare string would otherwise throw a raw TypeError instead of a typed error.
218
+ function assertArray(value, field) {
219
+ if (!Array.isArray(value)) {
220
+ throw new CorasValidationError("invalid_config", `${field} must be an array.`, field);
221
+ }
222
+ }
201
223
  function rejectUnknownKeys(object, allowed, context) {
202
224
  for (const key of Object.keys(object)) {
203
225
  if (!allowed.includes(key)) {
@@ -244,9 +266,15 @@ export function validateConfig(config, strict) {
244
266
  }
245
267
  if (config.colorScheme != null &&
246
268
  !colorSchemes.has(config.colorScheme)) {
247
- throw new CorasValidationError("invalid_color_scheme", `Unsupported colorScheme "${config.colorScheme}". Valid values: light, dark, system, inherit.`, "config.colorScheme");
269
+ throw new CorasValidationError("invalid_color_scheme", `Unsupported colorScheme "${config.colorScheme}". Valid values: light, dark, inherit.`, "config.colorScheme");
270
+ }
271
+ if (config.loyaltyPointsEnabled != null) {
272
+ if (typeof config.loyaltyPointsEnabled !== "boolean") {
273
+ throw new CorasValidationError("invalid_config", "config.loyaltyPointsEnabled must be a boolean.", "config.loyaltyPointsEnabled");
274
+ }
248
275
  }
249
276
  if (config.allowedLocales != null) {
277
+ assertArray(config.allowedLocales, "config.allowedLocales");
250
278
  normalized.allowedLocales = config.allowedLocales.map((value) => {
251
279
  const locale = normalizeLocale(value);
252
280
  if (!locale) {
@@ -256,6 +284,7 @@ export function validateConfig(config, strict) {
256
284
  });
257
285
  }
258
286
  if (config.allowedCurrencies != null) {
287
+ assertArray(config.allowedCurrencies, "config.allowedCurrencies");
259
288
  normalized.allowedCurrencies = config.allowedCurrencies.map((value) => {
260
289
  const currency = normalizeCurrency(value);
261
290
  if (!currency) {
@@ -269,40 +298,67 @@ export function validateConfig(config, strict) {
269
298
  export function validatePageParams(page, params, strict) {
270
299
  if (params == null)
271
300
  return {};
272
- if (typeof params !== "object") {
301
+ // An array is `typeof "object"`, but it is never a valid params bag: accepting
302
+ // one would silently ignore every param the host meant to pass.
303
+ if (typeof params !== "object" || Array.isArray(params)) {
273
304
  throw new CorasValidationError("invalid_page_params", `params for page "${page}" must be an object.`, "params");
274
305
  }
275
306
  const allowed = PAGE_PARAM_KEYS[page];
276
307
  if (strict) {
277
308
  rejectUnknownKeys(params, allowed, "params");
278
309
  }
279
- // `details.id` is the public id and must be a non-empty string when present.
280
- if (page === "details" && "id" in params) {
281
- const id = params.id;
282
- if (id != null && (typeof id !== "string" || id.length === 0)) {
283
- throw new CorasValidationError("invalid_page_params", "params.id must be a non-empty string.", "params.id");
284
- }
310
+ // Type-check every param the page accepts. Outside strict mode unknown keys
311
+ // are ignored rather than rejected, so only `allowed` keys are checked.
312
+ for (const key of allowed) {
313
+ const value = params[key];
314
+ // An explicit `undefined`/`null` clears the param back to its default.
315
+ if (value == null)
316
+ continue;
317
+ assertParamKind(page, key, value);
285
318
  }
286
- // `payment.metadata` is a free-form bag of client data; its keys are
287
- // arbitrary, but every value must be a string so it can be serialized for
288
- // the API forward.
289
- if (page === "payment" && "metadata" in params) {
290
- const metadata = params.metadata;
291
- if (metadata != null) {
292
- const isPlainObject = typeof metadata === "object" && !Array.isArray(metadata);
319
+ return params;
320
+ }
321
+ function assertParamKind(page, key, value) {
322
+ const invalid = (expected) => {
323
+ throw new CorasValidationError("invalid_page_params", `params.${key} for page "${page}" must be ${expected}.`, `params.${key}`);
324
+ };
325
+ switch (PAGE_PARAM_KINDS[key] ?? "string") {
326
+ case "number":
327
+ if (typeof value !== "number" || !Number.isFinite(value))
328
+ invalid("a number");
329
+ break;
330
+ case "boolean":
331
+ if (typeof value !== "boolean")
332
+ invalid("a boolean");
333
+ break;
334
+ case "date":
335
+ // Pages clear a date by emitting `""` (see the landing page's date reset),
336
+ // which means the same as omitting the param: reset it to its default.
337
+ if (typeof value !== "string" || (value !== "" && !ISO_DATE.test(value)))
338
+ invalid("a yyyy-MM-dd date");
339
+ break;
340
+ case "metadata": {
341
+ // A free-form bag of client data; its keys are arbitrary, but every value
342
+ // must be a string so it can be serialized for the API forward.
343
+ const isPlainObject = typeof value === "object" && !Array.isArray(value);
293
344
  if (!isPlainObject ||
294
- !Object.values(metadata).every((value) => typeof value === "string")) {
295
- throw new CorasValidationError("invalid_page_params", "params.metadata must be an object of string values.", "params.metadata");
296
- }
345
+ !Object.values(value).every((v) => typeof v === "string"))
346
+ invalid("an object of string values");
347
+ break;
297
348
  }
349
+ default:
350
+ if (typeof value !== "string")
351
+ invalid("a string");
352
+ break;
298
353
  }
299
- if (page === "suggestion-widget" && "limit" in params) {
300
- const limit = params.limit;
301
- if (limit != null && (typeof limit !== "number" || limit <= 0)) {
302
- throw new CorasValidationError("invalid_page_params", "params.limit must be a positive number.", "params.limit");
303
- }
304
- }
305
- return params;
354
+ // `id` is the public identifier and additionally carries the details route.
355
+ if (key === "id" && value.length === 0)
356
+ invalid("a non-empty string");
357
+ if (key === "limit" &&
358
+ (!Number.isInteger(value) ||
359
+ value <= 0 ||
360
+ value > MAX_WIDGET_LIMIT))
361
+ invalid(`a whole number between 1 and ${MAX_WIDGET_LIMIT}`);
306
362
  }
307
363
  /**
308
364
  * Validate and normalize the full mount options before anything is rendered.
@@ -1,9 +1,4 @@
1
1
  import type { ContrastPair } from "./contrast.js";
2
- /**
3
- * Default-theme contrast pairs validated in CI (plan F-004, P-058; pairs from
4
- * Appendix J). `tokens` lists the CSS variables each colour resolves from so a
5
- * stale check can confirm they still exist in the generated token sheet.
6
- */
7
2
  export type DefaultContrastPair = ContrastPair & {
8
3
  tokens: [string, string];
9
4
  };