@coras-io/embed 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +139 -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 +4 -1
  67. package/dist/components/seated-selector/seated-selector.component.js +90 -36
  68. package/dist/components/seated-selector/seated-selector.styles.js +89 -14
  69. package/dist/components/seats-io/seats-io.component.d.ts +2 -0
  70. package/dist/components/seats-io/seats-io.component.js +35 -2
  71. package/dist/components/seats-io/seats-io.constants.d.ts +5 -0
  72. package/dist/components/seats-io/seats-io.constants.js +12 -0
  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 -9
  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.styles.js +4 -0
  97. package/dist/containers/navbar/navbar.component.d.ts +5 -1
  98. package/dist/containers/navbar/navbar.component.js +38 -9
  99. package/dist/containers/navbar/navbar.styles.js +38 -12
  100. package/dist/contexts/basket/basket.provider.d.ts +1 -0
  101. package/dist/contexts/basket/basket.provider.js +31 -4
  102. package/dist/contexts/properties.context.d.ts +4 -4
  103. package/dist/core/api/middlewares/booking-session/booking-session.constants.d.ts +1 -0
  104. package/dist/core/api/middlewares/booking-session/booking-session.constants.js +1 -0
  105. package/dist/core/api/middlewares/booking-session/booking-session.js +18 -27
  106. package/dist/core/api/middlewares/booking-session/booking-session.utils.d.ts +4 -4
  107. package/dist/core/api/middlewares/booking-session/booking-session.utils.js +13 -10
  108. package/dist/core/api/queries.js +3 -2
  109. package/dist/core/color-scheme.d.ts +14 -0
  110. package/dist/core/color-scheme.js +48 -0
  111. package/dist/core/currency/constants.d.ts +1 -1
  112. package/dist/core/currency/constants.js +4 -3
  113. package/dist/core/i18n/date.js +20 -9
  114. package/dist/core/i18n/locale-codes.d.ts +2 -2
  115. package/dist/core/i18n/locale-codes.js +4 -0
  116. package/dist/core/images/constants.d.ts +0 -5
  117. package/dist/core/images/constants.js +0 -5
  118. package/dist/core/images/helpers.d.ts +8 -0
  119. package/dist/core/images/helpers.js +14 -6
  120. package/dist/core/images/srcset.js +2 -2
  121. package/dist/core/storybook/demo.d.ts +4 -0
  122. package/dist/core/storybook/demo.js +17 -0
  123. package/dist/core/types/locales.d.ts +7 -0
  124. package/dist/core/utils/datetime.js +10 -4
  125. package/dist/core/utils/locale.d.ts +8 -1
  126. package/dist/core/utils/locale.js +10 -0
  127. package/dist/helpers/helpers.d.ts +1 -1
  128. package/dist/index.d.ts +2 -3
  129. package/dist/index.js +1 -2
  130. package/dist/locales/ar-SA.d.ts +279 -0
  131. package/dist/locales/ar-SA.js +288 -0
  132. package/dist/locales/da-DK.d.ts +100 -82
  133. package/dist/locales/da-DK.js +104 -89
  134. package/dist/locales/de-DE.d.ts +100 -82
  135. package/dist/locales/de-DE.js +104 -89
  136. package/dist/locales/en-IE.d.ts +250 -232
  137. package/dist/locales/en-IE.js +253 -238
  138. package/dist/locales/en-US.d.ts +100 -82
  139. package/dist/locales/en-US.js +105 -90
  140. package/dist/locales/es-ES.d.ts +100 -82
  141. package/dist/locales/es-ES.js +104 -89
  142. package/dist/locales/fr-FR.d.ts +100 -82
  143. package/dist/locales/fr-FR.js +104 -89
  144. package/dist/locales/fr-MA.d.ts +100 -82
  145. package/dist/locales/fr-MA.js +104 -89
  146. package/dist/locales/is-IS.d.ts +100 -82
  147. package/dist/locales/is-IS.js +104 -89
  148. package/dist/locales/it-IT.d.ts +100 -82
  149. package/dist/locales/it-IT.js +104 -89
  150. package/dist/locales/ms-MY.d.ts +100 -82
  151. package/dist/locales/ms-MY.js +104 -89
  152. package/dist/locales/nb-NO.d.ts +100 -82
  153. package/dist/locales/nb-NO.js +104 -89
  154. package/dist/locales/pl-PL.d.ts +100 -82
  155. package/dist/locales/pl-PL.js +104 -89
  156. package/dist/locales/pt-PT.d.ts +100 -82
  157. package/dist/locales/pt-PT.js +104 -89
  158. package/dist/locales/sv-SE.d.ts +100 -82
  159. package/dist/locales/sv-SE.js +104 -89
  160. package/dist/locales/th-TH.d.ts +100 -82
  161. package/dist/locales/th-TH.js +104 -89
  162. package/dist/locales/tr-TR.d.ts +100 -82
  163. package/dist/locales/tr-TR.js +104 -89
  164. package/dist/locales/zh-CN.d.ts +100 -82
  165. package/dist/locales/zh-CN.js +105 -90
  166. package/dist/locales/zh-HK.d.ts +279 -0
  167. package/dist/locales/zh-HK.js +288 -0
  168. package/dist/mixins/coinbase.mixin.js +1 -1
  169. package/dist/mixins/error-modal.styles.js +2 -2
  170. package/dist/mixins/location-search/location-search.js +3 -3
  171. package/dist/mixins/location-search/location-search.styles.js +6 -5
  172. package/dist/sdk/chrome.d.ts +9 -3
  173. package/dist/sdk/chrome.js +21 -9
  174. package/dist/sdk/element-adapter.js +11 -8
  175. package/dist/sdk/mount.d.ts +3 -3
  176. package/dist/sdk/mount.js +150 -42
  177. package/dist/sdk/navigation-intent.js +3 -0
  178. package/dist/sdk/page-registry.d.ts +14 -2
  179. package/dist/sdk/page-registry.js +33 -13
  180. package/dist/sdk/theme.d.ts +1 -15
  181. package/dist/sdk/theme.js +4 -39
  182. package/dist/sdk/types.d.ts +55 -24
  183. package/dist/sdk/url.d.ts +24 -3
  184. package/dist/sdk/url.js +66 -27
  185. package/dist/sdk/validation.d.ts +0 -7
  186. package/dist/sdk/validation.js +105 -47
  187. package/dist/styles/contrast-fixtures.d.ts +0 -5
  188. package/dist/styles/contrast-fixtures.js +79 -9
  189. package/dist/styles/contrast.js +4 -3
  190. package/dist/styles/defaults/component.js +17 -1
  191. package/dist/styles/primitives.d.ts +0 -10
  192. package/dist/styles/primitives.js +2 -12
  193. package/dist/styles/tokens.d.ts +1 -1
  194. package/dist/styles/tokens.generated.d.ts +1 -1
  195. package/dist/styles/tokens.generated.js +1 -1
  196. package/dist/styles/tokens.metadata.d.ts +1 -1
  197. package/dist/styles/tokens.metadata.js +360 -90
  198. package/dist/views/details-page/details-page.component.d.ts +5 -5
  199. package/dist/views/details-page/details-page.component.js +97 -71
  200. package/dist/views/details-page/details-page.styles.js +64 -9
  201. package/dist/views/details-page/partials/theater-description/theater-description.component.js +5 -0
  202. package/dist/views/help-page/help-page.component.d.ts +1 -0
  203. package/dist/views/help-page/help-page.component.js +11 -4
  204. package/dist/views/help-page/help-page.styles.js +21 -8
  205. package/dist/views/landing-page/landing-page.component.d.ts +4 -4
  206. package/dist/views/landing-page/landing-page.component.js +65 -65
  207. package/dist/views/landing-page/landing-page.styles.js +70 -3
  208. package/dist/views/payment-page/partials/activity-details/activity-details.styles.js +10 -33
  209. package/dist/views/payment-page/partials/billing-address/billing-address.styles.js +4 -9
  210. package/dist/views/payment-page/partials/confirmation/confirmation.styles.js +6 -6
  211. package/dist/views/payment-page/partials/hotel-pickup-list/hotel-pickup-list.styles.js +4 -4
  212. package/dist/views/payment-page/partials/payment-type-selector/payment-type-selector.styles.js +1 -0
  213. package/dist/views/payment-page/partials/stripe/stripe.component.d.ts +1 -1
  214. package/dist/views/payment-page/partials/stripe/stripe.component.js +11 -12
  215. package/dist/views/payment-page/partials/stripe/stripe.constants.d.ts +2 -15
  216. package/dist/views/payment-page/partials/stripe/stripe.constants.js +4 -17
  217. package/dist/views/payment-page/partials/stripe/stripe.styles.js +16 -23
  218. package/dist/views/payment-page/partials/stripe/stripe.utils.d.ts +25 -14
  219. package/dist/views/payment-page/partials/stripe/stripe.utils.js +111 -62
  220. package/dist/views/payment-page/partials/terms-checkbox/terms-checkbox.styles.js +1 -1
  221. package/dist/views/payment-page/payment-page.component.d.ts +1 -0
  222. package/dist/views/payment-page/payment-page.component.js +19 -9
  223. package/dist/views/payment-page/payment-page.styles.js +37 -32
  224. package/dist/views/payment-page/payment-page.utils.d.ts +7 -0
  225. package/dist/views/payment-page/payment-page.utils.js +29 -14
  226. package/dist/views/payment-page/payment-page.validators.js +9 -3
  227. package/dist/views/payment-page/services/payment-api.service.d.ts +1 -1
  228. package/dist/views/search-page/search-page.component.d.ts +1 -0
  229. package/dist/views/search-page/search-page.component.js +12 -6
  230. package/dist/views/search-page/search-page.styles.js +24 -2
  231. package/dist/views/suggestion-widget/suggestion-widget.styles.js +1 -1
  232. package/package.json +62 -75
  233. package/dist/custom-elements.json +0 -37378
  234. package/dist/sdk/meta.d.ts +0 -7
  235. package/dist/sdk/meta.js +0 -9
  236. package/dist/styles/tokens.css +0 -359
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,9 +176,7 @@ 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
@@ -170,6 +184,9 @@ function validateAssetsUrl(value) {
170
184
  const THEME_KEYS = [
171
185
  ...FIELDS.map((field) => field.key),
172
186
  "logo",
187
+ "logoDark",
188
+ "navbarLogo",
189
+ "footerLogo",
173
190
  "fontFaces",
174
191
  "dark",
175
192
  "schemaVersion",
@@ -198,6 +215,13 @@ function validateTheme(theme, strict) {
198
215
  }
199
216
  }
200
217
  }
218
+ // Guards a `.map()` over host-supplied config: a JavaScript caller passing a
219
+ // bare string would otherwise throw a raw TypeError instead of a typed error.
220
+ function assertArray(value, field) {
221
+ if (!Array.isArray(value)) {
222
+ throw new CorasValidationError("invalid_config", `${field} must be an array.`, field);
223
+ }
224
+ }
201
225
  function rejectUnknownKeys(object, allowed, context) {
202
226
  for (const key of Object.keys(object)) {
203
227
  if (!allowed.includes(key)) {
@@ -244,9 +268,15 @@ export function validateConfig(config, strict) {
244
268
  }
245
269
  if (config.colorScheme != null &&
246
270
  !colorSchemes.has(config.colorScheme)) {
247
- throw new CorasValidationError("invalid_color_scheme", `Unsupported colorScheme "${config.colorScheme}". Valid values: light, dark, system, inherit.`, "config.colorScheme");
271
+ throw new CorasValidationError("invalid_color_scheme", `Unsupported colorScheme "${config.colorScheme}". Valid values: light, dark, inherit.`, "config.colorScheme");
272
+ }
273
+ if (config.loyaltyPointsEnabled != null) {
274
+ if (typeof config.loyaltyPointsEnabled !== "boolean") {
275
+ throw new CorasValidationError("invalid_config", "config.loyaltyPointsEnabled must be a boolean.", "config.loyaltyPointsEnabled");
276
+ }
248
277
  }
249
278
  if (config.allowedLocales != null) {
279
+ assertArray(config.allowedLocales, "config.allowedLocales");
250
280
  normalized.allowedLocales = config.allowedLocales.map((value) => {
251
281
  const locale = normalizeLocale(value);
252
282
  if (!locale) {
@@ -256,6 +286,7 @@ export function validateConfig(config, strict) {
256
286
  });
257
287
  }
258
288
  if (config.allowedCurrencies != null) {
289
+ assertArray(config.allowedCurrencies, "config.allowedCurrencies");
259
290
  normalized.allowedCurrencies = config.allowedCurrencies.map((value) => {
260
291
  const currency = normalizeCurrency(value);
261
292
  if (!currency) {
@@ -269,40 +300,67 @@ export function validateConfig(config, strict) {
269
300
  export function validatePageParams(page, params, strict) {
270
301
  if (params == null)
271
302
  return {};
272
- if (typeof params !== "object") {
303
+ // An array is `typeof "object"`, but it is never a valid params bag: accepting
304
+ // one would silently ignore every param the host meant to pass.
305
+ if (typeof params !== "object" || Array.isArray(params)) {
273
306
  throw new CorasValidationError("invalid_page_params", `params for page "${page}" must be an object.`, "params");
274
307
  }
275
308
  const allowed = PAGE_PARAM_KEYS[page];
276
309
  if (strict) {
277
310
  rejectUnknownKeys(params, allowed, "params");
278
311
  }
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
- }
312
+ // Type-check every param the page accepts. Outside strict mode unknown keys
313
+ // are ignored rather than rejected, so only `allowed` keys are checked.
314
+ for (const key of allowed) {
315
+ const value = params[key];
316
+ // An explicit `undefined`/`null` clears the param back to its default.
317
+ if (value == null)
318
+ continue;
319
+ assertParamKind(page, key, value);
285
320
  }
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);
321
+ return params;
322
+ }
323
+ function assertParamKind(page, key, value) {
324
+ const invalid = (expected) => {
325
+ throw new CorasValidationError("invalid_page_params", `params.${key} for page "${page}" must be ${expected}.`, `params.${key}`);
326
+ };
327
+ switch (PAGE_PARAM_KINDS[key] ?? "string") {
328
+ case "number":
329
+ if (typeof value !== "number" || !Number.isFinite(value))
330
+ invalid("a number");
331
+ break;
332
+ case "boolean":
333
+ if (typeof value !== "boolean")
334
+ invalid("a boolean");
335
+ break;
336
+ case "date":
337
+ // Pages clear a date by emitting `""` (see the landing page's date reset),
338
+ // which means the same as omitting the param: reset it to its default.
339
+ if (typeof value !== "string" || (value !== "" && !ISO_DATE.test(value)))
340
+ invalid("a yyyy-MM-dd date");
341
+ break;
342
+ case "metadata": {
343
+ // A free-form bag of client data; its keys are arbitrary, but every value
344
+ // must be a string so it can be serialized for the API forward.
345
+ const isPlainObject = typeof value === "object" && !Array.isArray(value);
293
346
  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
- }
347
+ !Object.values(value).every((v) => typeof v === "string"))
348
+ invalid("an object of string values");
349
+ break;
297
350
  }
351
+ default:
352
+ if (typeof value !== "string")
353
+ invalid("a string");
354
+ break;
298
355
  }
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;
356
+ // `id` is the public identifier and additionally carries the details route.
357
+ if (key === "id" && value.length === 0)
358
+ invalid("a non-empty string");
359
+ if (key === "limit" &&
360
+ (!Number.isInteger(value) ||
361
+ value <= 0 ||
362
+ value > MAX_WIDGET_LIMIT))
363
+ invalid(`a whole number between 1 and ${MAX_WIDGET_LIMIT}`);
306
364
  }
307
365
  /**
308
366
  * 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
  };
@@ -17,17 +17,17 @@ export const DEFAULT_CONTRAST_PAIRS = [
17
17
  tokens: ["--coras-color-text", "--coras-color-background"],
18
18
  },
19
19
  {
20
- label: "Secondary text on page background",
21
- foreground: "#666666",
20
+ label: "Muted/label text on page background",
21
+ foreground: "#888888",
22
22
  background: "#ffffff",
23
- threshold: "AA-normal",
24
- tokens: ["--coras-color-text-secondary", "--coras-color-background"],
23
+ threshold: "AA-large",
24
+ tokens: ["--coras-color-text-muted", "--coras-color-background"],
25
25
  },
26
26
  {
27
- label: "Heading/secondary brand text on background",
27
+ label: "Brand accent on background",
28
28
  foreground: "#161c44",
29
29
  background: "#ffffff",
30
- threshold: "AA-normal",
30
+ threshold: "AA-large",
31
31
  tokens: ["--coras-brand-secondary", "--coras-color-background"],
32
32
  },
33
33
  {
@@ -35,14 +35,14 @@ export const DEFAULT_CONTRAST_PAIRS = [
35
35
  foreground: "#888888",
36
36
  background: "#fcfcff",
37
37
  threshold: "AA-large",
38
- tokens: ["--coras-color-text-muted", "--coras-color-surface"],
38
+ tokens: ["--coras-color-text-muted", "--coras-color-card"],
39
39
  },
40
40
  {
41
41
  label: "Error alert text on error alert background",
42
42
  foreground: "#c9224a",
43
- background: "#fae9ed",
43
+ background: "#fae8ec",
44
44
  threshold: "AA-normal",
45
- tokens: ["--coras-color-error", "--coras-color-error-surface"],
45
+ tokens: ["--coras-color-error", "--coras-color-error-background"],
46
46
  },
47
47
  {
48
48
  label: "Focus ring on background",
@@ -58,4 +58,74 @@ export const DEFAULT_CONTRAST_PAIRS = [
58
58
  threshold: "AA-normal",
59
59
  tokens: ["--coras-navbar-text-color", "--coras-navbar-background"],
60
60
  },
61
+ {
62
+ label: "Body text on page background (dark)",
63
+ foreground: "#ffffff",
64
+ background: "#101216",
65
+ threshold: "AA-normal",
66
+ tokens: ["--coras-color-text", "--coras-color-background"],
67
+ },
68
+ {
69
+ label: "Muted/label text on page background (dark)",
70
+ foreground: "#cccccc",
71
+ background: "#101216",
72
+ threshold: "AA-large",
73
+ tokens: ["--coras-color-text-muted", "--coras-color-background"],
74
+ },
75
+ {
76
+ label: "Muted/placeholder text on surface (dark)",
77
+ foreground: "#cccccc",
78
+ background: "#191c22",
79
+ threshold: "AA-large",
80
+ tokens: ["--coras-color-text-muted", "--coras-color-card"],
81
+ },
82
+ {
83
+ label: "Focus ring on background (dark)",
84
+ foreground: "#4657d4",
85
+ background: "#101216",
86
+ threshold: "AA-large",
87
+ tokens: ["--coras-color-focus-ring", "--coras-color-background"],
88
+ },
89
+ {
90
+ label: "Navbar text on navbar background (dark)",
91
+ foreground: "#ffffff",
92
+ background: "#101216",
93
+ threshold: "AA-normal",
94
+ tokens: ["--coras-navbar-text-color", "--coras-navbar-background"],
95
+ },
96
+ {
97
+ label: "Footer text on footer background (dark)",
98
+ foreground: "#ffffff",
99
+ background: "#191c22",
100
+ threshold: "AA-normal",
101
+ tokens: ["--coras-footer-text-color", "--coras-footer-background-color"],
102
+ },
103
+ {
104
+ label: "Text button on surface (dark)",
105
+ foreground: "#6279f9",
106
+ background: "#191c22",
107
+ threshold: "AA-normal",
108
+ tokens: ["--coras-color-text-button", "--coras-color-card"],
109
+ },
110
+ {
111
+ label: "Link on page background (dark)",
112
+ foreground: "#6279f9",
113
+ background: "#101216",
114
+ threshold: "AA-normal",
115
+ tokens: ["--coras-color-link", "--coras-color-background"],
116
+ },
117
+ {
118
+ label: "Error alert text on error alert background (dark)",
119
+ foreground: "#f5748a",
120
+ background: "#442e36",
121
+ threshold: "AA-normal",
122
+ tokens: ["--coras-color-error", "--coras-color-error-background"],
123
+ },
124
+ {
125
+ label: "Info alert text on info alert background (dark)",
126
+ foreground: "#ba86e1",
127
+ background: "#373045",
128
+ threshold: "AA-normal",
129
+ tokens: ["--coras-color-info", "--coras-color-info-background"],
130
+ },
61
131
  ];
@@ -1,6 +1,7 @@
1
- // WCAG contrast utilities (plan F-004, P-058). Pure functions reused by the
2
- // default-theme CI check and Coras Studio's live contrast panel (exposed via
3
- // the `@coras-io/embed/contrast` entry).
1
+ // WCAG contrast utilities (plan F-004, P-058). Internal to the embed: they back
2
+ // the default-theme CI check, which needs a browser to resolve any CSS colour
3
+ // (`oklch()`, named colours) by rasterizing it. `@coras-io/brand-tokens` carries
4
+ // the portable, parser-based equivalents for hosts and the Studio.
4
5
  const MIN_RATIO = {
5
6
  // AA normal text and AA UI/large both per WCAG 1.4.3 / 1.4.11.
6
7
  "AA-normal": 4.5,
@@ -17,6 +17,22 @@ export default css `
17
17
  box-sizing: inherit;
18
18
  }
19
19
 
20
+ :host button,
21
+ :host input,
22
+ :host select,
23
+ :host textarea {
24
+ font-family: inherit;
25
+ }
26
+
27
+ :host h1,
28
+ :host h2,
29
+ :host h3,
30
+ :host h4,
31
+ :host h5,
32
+ :host h6 {
33
+ font-family: var(--coras-font-family-heading);
34
+ }
35
+
20
36
  [hidden] {
21
37
  display: none !important;
22
38
  }
@@ -26,6 +42,6 @@ export default css `
26
42
  :host a:active {
27
43
  text-decoration: none;
28
44
  font-weight: 600;
29
- color: var(--coras-color-primary-500);
45
+ color: var(--coras-color-primary);
30
46
  }
31
47
  `;
@@ -1,14 +1,4 @@
1
- /**
2
- * Shared CSS primitives (plan F-004, P-063). Components compose these instead
3
- * of re-authoring focus rings, screen-reader helpers, or skeleton shimmer, so
4
- * accessibility and branding stay consistent. All values resolve from the
5
- * semantic token tier.
6
- */
7
- /** Visible focus ring using the brand focus token. */
8
1
  export declare const focusRing: import("lit").CSSResult;
9
- /** Accessible "visually hidden" helper that remains in the accessibility tree. */
10
2
  export declare const visuallyHidden: import("lit").CSSResult;
11
- /** Loading skeleton shimmer; respects reduced-motion. */
12
3
  export declare const skeleton: import("lit").CSSResult;
13
- /** Reset for native controls used as branded buttons/fields. */
14
4
  export declare const controlReset: import("lit").CSSResult;