@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
package/dist/sdk/mount.js CHANGED
@@ -3,9 +3,9 @@ import { DEFAULT_CURRENCY, DEFAULT_LOCALE } from "../views/constants.js";
3
3
  import { createDiagnostics, themeHasNoColours } from "./diagnostics.js";
4
4
  import { attachCallbackBridge, } from "./element-adapter.js";
5
5
  import { createChrome } from "./chrome.js";
6
- import { applyConfig, applyParams, captureParamDefaults, defaultPageRegistry, } from "./page-registry.js";
6
+ import { applyConfig, applyParams, captureConfigDefaults, captureParamDefaults, defaultPageRegistry, } from "./page-registry.js";
7
7
  import { applyColorScheme, applyTheme, clearTheme } from "./theme.js";
8
- import { safeUrl } from "@coras-io/brand-tokens";
8
+ import { safeUrl, migrateBrandConfig, } from "@coras-io/brand-tokens";
9
9
  import { ensureTokensInjected } from "../styles/tokens.js";
10
10
  import { redactValue, toPublicErrorDetail } from "./redaction.js";
11
11
  import { assertValidPage, CorasValidationError, validateConfig, validateMountOptions, validatePageParams, } from "./validation.js";
@@ -14,6 +14,23 @@ function isThenable(value) {
14
14
  value !== null &&
15
15
  typeof value.then === "function");
16
16
  }
17
+ // `apiUrl` and `distributorId` fix the mount's identity: the API client is
18
+ // built once around them (base.mixin), so an update cannot change them. The
19
+ // public type already omits them; this guards JavaScript callers.
20
+ const IMMUTABLE_CONFIG_KEYS = ["apiUrl", "distributorId"];
21
+ // Reject an update only when it would actually change an identity key, not when
22
+ // it merely restates the mounted value. Hosts commonly rebuild one config object
23
+ // and pass it to both `mount()` and `update()` (every framework wrapper does),
24
+ // so re-sending the unchanged `apiUrl`/`distributorId` must be a no-op rather
25
+ // than an error; only a genuinely different value is rejected.
26
+ function assertIdentityUnchanged(patch, current) {
27
+ const provided = patch;
28
+ for (const key of IMMUTABLE_CONFIG_KEYS) {
29
+ if (key in provided && provided[key] !== current[key]) {
30
+ throw new CorasValidationError("invalid_config", `config.${key} cannot be changed after mount; unmount and mount again to switch ${key === "apiUrl" ? "the API endpoint" : "distributor"}.`, `config.${key}`);
31
+ }
32
+ }
33
+ }
17
34
  /**
18
35
  * Create a `mount` function bound to a page registry. The public `mount` uses
19
36
  * the real registry; tests inject a lightweight registry so lifecycle behavior
@@ -38,11 +55,21 @@ export function createMountWith(registry) {
38
55
  const root = document.createElement("div");
39
56
  root.setAttribute("data-coras-root", "");
40
57
  // Token system is defined on the container scope and inherits into every
41
- // nested Web Component's shadow tree (plan F-004). Adopting into the
42
- // document is idempotent and shared across mounts.
43
- ensureTokensInjected(container.ownerDocument ?? document);
58
+ // nested Web Component's shadow tree (plan F-004). Adopt into the container's
59
+ // root node, which is its host ShadowRoot when mounted inside one - adopting
60
+ // into ownerDocument would leave a shadow-hosted mount with only the
61
+ // @property initials, losing the entire token system. `ensureTokensInjected`
62
+ // already accepts a ShadowRoot; give it the real root.
63
+ const containerRoot = container.getRootNode();
64
+ ensureTokensInjected(containerRoot instanceof ShadowRoot
65
+ ? containerRoot
66
+ : (container.ownerDocument ?? document));
44
67
  let currentPage = validated.page;
45
68
  let currentConfig = validated.config;
69
+ const themeLogos = () => {
70
+ const t = migrateBrandConfig(currentConfig.theme ?? {});
71
+ return { logo: t.logo, logoDark: t.logoDark, navbarLogo: t.navbarLogo };
72
+ };
46
73
  let currentParams = validated.params;
47
74
  let currentChrome = options.chrome;
48
75
  if (themeHasNoColours(currentConfig.theme))
@@ -53,8 +80,10 @@ export function createMountWith(registry) {
53
80
  container.appendChild(root);
54
81
  let destroyed = false;
55
82
  let generation = 0;
83
+ let chromeGeneration = 0;
56
84
  let pageElement = null;
57
85
  let pageParamDefaults = {};
86
+ let pageConfigDefaults = {};
58
87
  let chromeElements = [];
59
88
  let readySettled = false;
60
89
  let resolveReady;
@@ -93,7 +122,9 @@ export function createMountWith(registry) {
93
122
  const detail = toPublicErrorDetail(error, currentPage);
94
123
  // Per-mount logging only; the SDK never writes unconditional console output.
95
124
  options.logger?.error?.(error);
96
- options.onError?.(detail);
125
+ // Deliver to `onError` via the `coras:error` event only: the callback
126
+ // bridge routes it to `onError`, so calling `onError` here as well would
127
+ // fire it twice for one error.
97
128
  fireEvent(root, "coras:error", detail);
98
129
  emitObservability("coras.error", { code: detail.code });
99
130
  }
@@ -103,13 +134,32 @@ export function createMountWith(registry) {
103
134
  }
104
135
  chromeElements = [];
105
136
  }
137
+ // Push `currentConfig.theme` onto the root and any live navbar. Chrome is
138
+ // not rebuilt on a theme-only update, so the default logo (light + dark)
139
+ // goes straight to the navbar.
140
+ function syncThemeToRoot() {
141
+ clearTheme(root);
142
+ applyTheme(root, currentConfig.theme);
143
+ for (const element of chromeElements) {
144
+ if (element.tagName === "CORAS-NAVBAR") {
145
+ const nav = element;
146
+ const logos = themeLogos();
147
+ nav.logo = safeUrl(logos.logo);
148
+ nav.logoDark = safeUrl(logos.logoDark);
149
+ nav.navbarLogo = safeUrl(logos.navbarLogo);
150
+ }
151
+ }
152
+ }
106
153
  async function renderPage(targetPage, generationToken) {
107
154
  const entry = registry[targetPage];
108
155
  await entry.load();
109
156
  if (destroyed || generationToken !== generation)
110
157
  return;
111
158
  const element = document.createElement(entry.tagName);
112
- applyConfig(element, currentConfig);
159
+ // Capture element defaults before applyConfig writes to them, so a later
160
+ // update that clears an optional config value can restore the default.
161
+ const configDefaults = captureConfigDefaults(element);
162
+ applyConfig(element, currentConfig, configDefaults);
113
163
  // Preview is a mount-time rendering mode, not config or a param, so it is
114
164
  // set directly on the element; pages that honor it read their own property.
115
165
  element.preview = preview;
@@ -118,9 +168,7 @@ export function createMountWith(registry) {
118
168
  // Chrome is rebuilt per page rather than moved: the navbar/footer consume
119
169
  // the page's context (locale, dispatch, current page…) and re-parenting a
120
170
  // context consumer across providers leaves those bindings stale.
121
- const chrome = await createChrome(currentChrome, {
122
- logo: currentConfig.theme?.logo,
123
- });
171
+ const chrome = await createChrome(currentChrome, themeLogos());
124
172
  if (destroyed || generationToken !== generation)
125
173
  return;
126
174
  for (const chromeElement of chrome) {
@@ -131,6 +179,7 @@ export function createMountWith(registry) {
131
179
  }
132
180
  pageElement = element;
133
181
  pageParamDefaults = paramDefaults;
182
+ pageConfigDefaults = configDefaults;
134
183
  chromeElements = chrome;
135
184
  root.appendChild(element);
136
185
  root.dataset.corasPage = targetPage;
@@ -138,28 +187,49 @@ export function createMountWith(registry) {
138
187
  try {
139
188
  await element.updateComplete;
140
189
  }
141
- catch {
142
- // Page render errors are surfaced through the page's own error paths.
190
+ catch (error) {
191
+ if (destroyed || generationToken !== generation)
192
+ return;
193
+ // A rejected first render must not resolve `ready` as a success or
194
+ // emit `coras:ready`; surface the failure instead.
195
+ reportError(error);
196
+ if (!readySettled) {
197
+ readySettled = true;
198
+ rejectReady(error);
199
+ }
200
+ return;
143
201
  }
144
202
  }
145
203
  if (destroyed || generationToken !== generation)
146
204
  return;
147
205
  root.dataset.corasMounted = "true";
206
+ // `coras:ready` is the first-completed-render signal, so emit it (and
207
+ // resolve `ready`) exactly once; later navigations re-render without
208
+ // re-firing it.
148
209
  if (!readySettled) {
149
210
  readySettled = true;
150
211
  resolveReady();
212
+ fireEvent(root, "coras:ready", { page: targetPage });
151
213
  }
152
- fireEvent(root, "coras:ready", { page: targetPage });
153
214
  emitObservability("coras.page-loaded", { page: targetPage });
154
215
  }
155
216
  const bridge = attachCallbackBridge(root, options, {
156
217
  onListenerError: (error) => options.logger?.error?.(error),
157
218
  });
158
- function startRender(targetPage) {
219
+ function startRender(targetPage, rollback) {
159
220
  const token = ++generation;
221
+ // A fresh render owns chrome from here on; invalidate any in-flight
222
+ // chrome-only update so its late resolve can't append to the new page.
223
+ chromeGeneration += 1;
160
224
  renderPage(targetPage, token).catch((error) => {
161
225
  if (destroyed || token !== generation)
162
226
  return;
227
+ // `renderPage` only throws before it swaps the DOM (the page chunk or
228
+ // the chrome never loaded), so the previous page is still on screen.
229
+ // Restore the state that describes it, or `app.state` would report a
230
+ // page that never rendered. Roll back before reporting, so the error
231
+ // and any state read from its handler agree.
232
+ rollback?.();
163
233
  reportError(error);
164
234
  if (!readySettled) {
165
235
  readySettled = true;
@@ -201,61 +271,94 @@ export function createMountWith(registry) {
201
271
  function applyUpdate(patch) {
202
272
  if (destroyed)
203
273
  return;
204
- // Reject an unknown page before mutating any state, so a bad update or
205
- // navigate can't leave currentPage pointing at a page the registry lacks.
274
+ // Phase 1 - validate the whole patch before mutating any state, so a
275
+ // failure (unknown page, invalid params/config, immutable field) leaves
276
+ // the mount's state and DOM exactly as they were.
206
277
  if (patch.page != null)
207
278
  assertValidPage(patch.page);
279
+ let nextConfig = currentConfig;
208
280
  if (patch.config) {
209
- currentConfig = validateConfig({ ...currentConfig, ...patch.config }, strict);
210
- if ("theme" in patch.config) {
211
- clearTheme(root);
212
- applyTheme(root, currentConfig.theme);
213
- // Chrome is not rebuilt on a theme-only update, so push the (possibly
214
- // changed) default logo to the live navbar directly.
215
- for (const element of chromeElements) {
216
- if (element.tagName === "CORAS-NAVBAR") {
217
- element.logo = safeUrl(currentConfig.theme?.logo);
218
- }
219
- }
220
- }
221
- // A colour-scheme flip is just the root's `color-scheme`; the
222
- // `light-dark()` pairs already carry both modes.
223
- if ("colorScheme" in patch.config) {
224
- applyColorScheme(root, currentConfig.colorScheme);
225
- }
281
+ assertIdentityUnchanged(patch.config, currentConfig);
282
+ nextConfig = validateConfig({ ...currentConfig, ...patch.config }, strict);
226
283
  }
227
284
  const pageChanged = patch.page != null && patch.page !== currentPage;
228
- const chromeChanged = patch.chrome !== undefined;
229
- if (chromeChanged)
230
- currentChrome = patch.chrome;
285
+ const targetPage = pageChanged
286
+ ? patch.page
287
+ : currentPage;
288
+ let nextParams = currentParams;
231
289
  if (pageChanged) {
232
- currentPage = patch.page;
233
- currentParams = validatePageParams(currentPage, patch.params, strict);
234
- startRender(currentPage);
235
- return;
290
+ nextParams = validatePageParams(targetPage, patch.params, strict);
236
291
  }
237
- if (patch.params) {
292
+ else if (patch.params) {
238
293
  // Params are the full desired state for the page: replace rather than
239
294
  // merge so params the host dropped (cleared) are reset to defaults by
240
295
  // applyParams instead of lingering.
241
- currentParams = validatePageParams(currentPage, patch.params, strict);
296
+ nextParams = validatePageParams(currentPage, patch.params, strict);
297
+ }
298
+ // Phase 2 - commit. Nothing below throws on invalid input.
299
+ const themeChanged = patch.config != null && "theme" in patch.config;
300
+ const colorSchemeChanged = patch.config != null && "colorScheme" in patch.config;
301
+ const chromeChanged = patch.chrome !== undefined;
302
+ const previous = {
303
+ page: currentPage,
304
+ params: currentParams,
305
+ config: currentConfig,
306
+ chrome: currentChrome,
307
+ };
308
+ currentConfig = nextConfig;
309
+ currentParams = nextParams;
310
+ if (chromeChanged)
311
+ currentChrome = patch.chrome;
312
+ if (themeChanged)
313
+ syncThemeToRoot();
314
+ // A colour-scheme flip is just the root's `color-scheme`; the
315
+ // `light-dark()` pairs already carry both modes.
316
+ if (colorSchemeChanged) {
317
+ applyColorScheme(root, currentConfig.colorScheme);
318
+ }
319
+ if (pageChanged) {
320
+ currentPage = targetPage;
321
+ // A page change renders asynchronously, so the rest of the patch is not
322
+ // durable until it lands: the new config reaches the DOM only through
323
+ // the new page element. If the render fails, undo the whole patch so an
324
+ // `update()` stays all-or-nothing rather than half-applied.
325
+ startRender(currentPage, () => {
326
+ currentPage = previous.page;
327
+ currentParams = previous.params;
328
+ currentConfig = previous.config;
329
+ currentChrome = previous.chrome;
330
+ if (themeChanged)
331
+ syncThemeToRoot();
332
+ if (colorSchemeChanged) {
333
+ applyColorScheme(root, currentConfig.colorScheme);
334
+ }
335
+ });
336
+ return;
242
337
  }
243
338
  // No page change: update the existing element in place when possible.
244
339
  if (pageElement) {
245
- applyConfig(pageElement, currentConfig);
340
+ applyConfig(pageElement, currentConfig, pageConfigDefaults);
246
341
  applyParams(pageElement, registry[currentPage], currentParams, pageParamDefaults);
247
342
  if (chromeChanged) {
248
343
  removeChrome();
249
- void createChrome(currentChrome, {
250
- logo: currentConfig.theme?.logo,
251
- }).then((chrome) => {
252
- if (destroyed || !pageElement)
344
+ const chromeToken = ++chromeGeneration;
345
+ void createChrome(currentChrome, themeLogos())
346
+ .then((chrome) => {
347
+ // A newer chrome update or a page render has superseded this one;
348
+ // discard the stale chrome instead of appending it.
349
+ if (destroyed ||
350
+ !pageElement ||
351
+ chromeToken !== chromeGeneration) {
352
+ for (const chromeElement of chrome)
353
+ chromeElement.remove();
253
354
  return;
355
+ }
254
356
  for (const chromeElement of chrome) {
255
357
  pageElement.appendChild(chromeElement);
256
358
  }
257
359
  chromeElements = chrome;
258
- });
360
+ })
361
+ .catch(reportError);
259
362
  }
260
363
  }
261
364
  else if (chromeChanged) {
@@ -283,6 +386,8 @@ export function createMountWith(registry) {
283
386
  if (navigateOptions?.emitNavigate) {
284
387
  // Programmatic navigation is tagged `host` so onNavigate handlers can
285
388
  // avoid re-driving the host router (loop prevention, ADR 0014).
389
+ // `page` and `params` are correlated by this method's own signature;
390
+ // the cast only re-states that for the un-narrowed event map.
286
391
  fireEvent(root, "coras:navigate", {
287
392
  page,
288
393
  params,
@@ -3,6 +3,9 @@ export function classifyPageUpdate(input, context) {
3
3
  const currency = input.currency ?? context.currency;
4
4
  const isNavigate = (input.page != null && input.page !== context.currentPage) ||
5
5
  input.href != null;
6
+ // The public details correlate `page` with `params`; here the two arrive as
7
+ // separate values whose pairing was already established by `validatePageParams`
8
+ // upstream, so the cast re-states an invariant rather than assuming one.
6
9
  if (isNavigate) {
7
10
  return {
8
11
  kind: "navigate",
@@ -14,8 +14,20 @@ export type PageEntry = {
14
14
  };
15
15
  export type PageRegistry = Record<CorasPageName, PageEntry>;
16
16
  export declare const defaultPageRegistry: PageRegistry;
17
- /** Apply normalized SDK config to a page (or chrome) element's properties. */
18
- export declare function applyConfig(element: ConfigurableElement, config: CorasConfig): void;
17
+ /**
18
+ * Snapshot the element's default values for every property an optional config
19
+ * field drives. Taken right after creation (before `applyConfig`) so a later
20
+ * update that drops an optional value resets the property to its default - i.e.
21
+ * "clears" it - instead of leaving the stale value in place. Mirrors
22
+ * `captureParamDefaults` for params.
23
+ */
24
+ export declare function captureConfigDefaults(element: ConfigurableElement): Record<string, unknown>;
25
+ /**
26
+ * Apply normalized SDK config to a page (or chrome) element's properties. An
27
+ * optional field the config omits resets its property to the captured default,
28
+ * so clearing `locale`/`currency`/etc. on an update actually takes effect.
29
+ */
30
+ export declare function applyConfig(element: ConfigurableElement, config: CorasConfig, defaults: Record<string, unknown>): void;
19
31
  /**
20
32
  * Snapshot the element's default values for every param the page exposes. Taken
21
33
  * right after creation (before any params are applied) so a later update can
@@ -40,22 +40,42 @@ export const defaultPageRegistry = {
40
40
  paramToProperty: identity("limit", "showExploreCard", "country", "city"),
41
41
  },
42
42
  };
43
- /** Apply normalized SDK config to a page (or chrome) element's properties. */
44
- export function applyConfig(element, config) {
43
+ /** Element properties driven by an optional config field, in config -> property form. */
44
+ const OPTIONAL_CONFIG_PROPERTIES = [
45
+ "locale",
46
+ "locales",
47
+ "currency",
48
+ "currencies",
49
+ "loyaltyPoints",
50
+ ];
51
+ /**
52
+ * Snapshot the element's default values for every property an optional config
53
+ * field drives. Taken right after creation (before `applyConfig`) so a later
54
+ * update that drops an optional value resets the property to its default - i.e.
55
+ * "clears" it - instead of leaving the stale value in place. Mirrors
56
+ * `captureParamDefaults` for params.
57
+ */
58
+ export function captureConfigDefaults(element) {
59
+ const defaults = {};
60
+ for (const property of OPTIONAL_CONFIG_PROPERTIES) {
61
+ defaults[property] = element[property];
62
+ }
63
+ return defaults;
64
+ }
65
+ /**
66
+ * Apply normalized SDK config to a page (or chrome) element's properties. An
67
+ * optional field the config omits resets its property to the captured default,
68
+ * so clearing `locale`/`currency`/etc. on an update actually takes effect.
69
+ */
70
+ export function applyConfig(element, config, defaults) {
45
71
  element.apiHost = config.apiUrl;
46
72
  element.distributorID = config.distributorId;
47
73
  element.assetsURL = config.assetsUrl;
48
- if (config.locale != null)
49
- element.locale = config.locale;
50
- if (config.allowedLocales != null)
51
- element.locales = config.allowedLocales;
52
- if (config.currency != null)
53
- element.currency = config.currency;
54
- if (config.allowedCurrencies != null)
55
- element.currencies = config.allowedCurrencies;
56
- if (config.loyaltyPointsEnabled != null) {
57
- element.loyaltyPoints = config.loyaltyPointsEnabled;
58
- }
74
+ element.locale = config.locale ?? defaults.locale;
75
+ element.locales = config.allowedLocales ?? defaults.locales;
76
+ element.currency = config.currency ?? defaults.currency;
77
+ element.currencies = config.allowedCurrencies ?? defaults.currencies;
78
+ element.loyaltyPoints = config.loyaltyPointsEnabled ?? defaults.loyaltyPoints;
59
79
  }
60
80
  /**
61
81
  * Snapshot the element's default values for every param the page exposes. Taken
@@ -1,19 +1,5 @@
1
1
  import type { CorasColorScheme, CorasTheme } from "./types.js";
2
- /**
3
- * The brand `theme` keys that map 1:1 to a public `--coras-brand-*` custom
4
- * property. Sourced from the shared engine so the SDK, plugin, and Studio agree.
5
- * `navbarShadow`, `logo`, and `fontFaces` are applied specially (below).
6
- */
7
- export declare const THEME_CSS_VARS: Record<"primary" | "secondary" | "background" | "surface" | "text" | "textMuted" | "radius" | "font" | "textButton" | "bodyIcon" | "buttonRadius" | "chipRadius" | "navbar" | "navbarText" | "navbarIcon" | "navbarBorder" | "footer" | "footerText" | "footerLink" | "error" | "success" | "warning" | "info", string>;
8
- /**
9
- * Set the mount root's colour scheme so the token sheet's `light-dark()` pairs
10
- * resolve correctly. `"system"` maps to `light dark` (follows the OS via
11
- * `prefers-color-scheme`); `"inherit"` overrides the sheet's default `light` so
12
- * the root inherits the host page's `color-scheme`. Cheap enough to call on its
13
- * own for a mode flip.
14
- */
2
+ export declare const THEME_CSS_VARS: Record<"primary" | "secondary" | "pageBackground" | "text" | "textMuted" | "link" | "icon" | "focusRing" | "scrim" | "cardBackground" | "cardBorder" | "fieldBackground" | "fieldBorder" | "menuBackground" | "menuBorder" | "divider" | "cardShadow" | "softShadow" | "primaryButton" | "primaryButtonText" | "primaryButtonHover" | "primaryButtonPressed" | "secondaryButton" | "secondaryButtonText" | "secondaryButtonHover" | "secondaryButtonPressed" | "textButton" | "primaryButtonBorder" | "secondaryButtonBorder" | "disabled" | "buttonShadow" | "buttonHoverOnLightFill" | "buttonHoverOnDarkFill" | "radius" | "buttonRadius" | "chipRadius" | "fieldRadius" | "searchRadius" | "font" | "headingFont" | "headingScale" | "logoHeight" | "navbarBackground" | "navbarBackgroundImage" | "navbarText" | "navbarIcon" | "navbarBorder" | "navbarShadow" | "footerBackground" | "footerBackgroundImage" | "footerText" | "footerLink" | "footerBorder" | "footerPaddingBottom" | "error" | "errorBackground" | "success" | "successBackground" | "warning" | "warningBackground" | "info" | "infoBackground" | "primaryTint" | "secondaryTint" | "neutral50" | "neutral100" | "neutral200" | "neutral300" | "neutral400" | "neutral500" | "neutral600" | "neutral700" | "neutral800" | "neutral900", string>;
15
3
  export declare function applyColorScheme(element: HTMLElement, scheme: CorasColorScheme | undefined): void;
16
- /** Apply a brand to an element as scoped `--coras-*` variables (P-052/P-061). */
17
- export declare function applyTheme(element: HTMLElement, theme: CorasTheme | undefined): void;
18
- /** Remove any brand variables/fonts previously set on an element. */
4
+ export declare function applyTheme(element: HTMLElement, input: CorasTheme | undefined): void;
19
5
  export declare function clearTheme(element: HTMLElement): void;
package/dist/sdk/theme.js CHANGED
@@ -1,56 +1,23 @@
1
- import { BRAND_CSS_VARS, brandVarValue, fontFacesToCSS, sanitizeBrandValue, } from "@coras-io/brand-tokens";
2
- /**
3
- * The brand `theme` keys that map 1:1 to a public `--coras-brand-*` custom
4
- * property. Sourced from the shared engine so the SDK, plugin, and Studio agree.
5
- * `navbarShadow`, `logo`, and `fontFaces` are applied specially (below).
6
- */
1
+ import { BRAND_CSS_VARS, BRAND_DARK_CSS_VARS, brandVarEntries, brandVarValue, fontFacesToCSS, migrateBrandConfig, } from "@coras-io/brand-tokens";
7
2
  export const THEME_CSS_VARS = BRAND_CSS_VARS;
8
3
  const THEME_KEYS = Object.keys(THEME_CSS_VARS);
9
- /**
10
- * Set the mount root's colour scheme so the token sheet's `light-dark()` pairs
11
- * resolve correctly. `"system"` maps to `light dark` (follows the OS via
12
- * `prefers-color-scheme`); `"inherit"` overrides the sheet's default `light` so
13
- * the root inherits the host page's `color-scheme`. Cheap enough to call on its
14
- * own for a mode flip.
15
- */
16
4
  export function applyColorScheme(element, scheme) {
17
5
  element.style.colorScheme =
18
- scheme === "dark"
19
- ? "dark"
20
- : scheme === "system"
21
- ? "light dark"
22
- : scheme === "inherit"
23
- ? "inherit"
24
- : "light";
6
+ scheme === "dark" ? "dark" : scheme === "inherit" ? "inherit" : "light";
25
7
  }
26
- /** Apply a brand to an element as scoped `--coras-*` variables (P-052/P-061). */
27
- export function applyTheme(element, theme) {
28
- if (!theme)
8
+ export function applyTheme(element, input) {
9
+ if (!input)
29
10
  return;
30
- // Each themeable var is emitted as a `light-dark()` pair (or a single value
31
- // when the field has no dark variant) via the shared engine, which sanitizes
32
- // both sides - an invalid value yields "" and is skipped, so the Coras default
33
- // (itself a light-dark pair) shows through. The dark side falls back to the
34
- // Coras dark default when the brand sets only the light value.
11
+ const theme = migrateBrandConfig(input);
35
12
  const dark = (theme.dark ?? {});
36
13
  for (const key of THEME_KEYS) {
37
14
  if (theme[key] == null && dark[key] == null)
38
15
  continue;
39
- const value = brandVarValue(key, theme[key], dark[key]);
40
- if (value)
41
- element.style.setProperty(THEME_CSS_VARS[key], value);
16
+ for (const [name, value] of brandVarEntries(key, theme[key], dark[key]))
17
+ element.style.setProperty(name, value);
42
18
  }
43
- if (theme.navbarShadow != null) {
44
- const shadow = sanitizeBrandValue("navbarShadow", theme.navbarShadow);
45
- if (shadow) {
46
- element.style.setProperty("--coras-brand-navbar-shadow", shadow);
47
- }
48
- }
49
- // The page background is opt-in (see the root rule in tokens.css): only an
50
- // explicit `background` paints the embed surface, so the embed otherwise
51
- // adopts the host page's background.
52
- if (theme.background != null || dark.background != null) {
53
- const pageBg = brandVarValue("background", theme.background, dark.background);
19
+ if (theme.pageBackground != null || dark.pageBackground != null) {
20
+ const pageBg = brandVarValue("pageBackground", theme.pageBackground, dark.pageBackground);
54
21
  if (pageBg)
55
22
  element.style.setProperty("--coras-page-background", pageBg);
56
23
  }
@@ -62,12 +29,13 @@ export function applyTheme(element, theme) {
62
29
  element.appendChild(style);
63
30
  }
64
31
  }
65
- /** Remove any brand variables/fonts previously set on an element. */
66
32
  export function clearTheme(element) {
67
33
  for (const key of THEME_KEYS) {
68
34
  element.style.removeProperty(THEME_CSS_VARS[key]);
69
35
  }
70
- element.style.removeProperty("--coras-brand-navbar-shadow");
36
+ for (const name of Object.values(BRAND_DARK_CSS_VARS)) {
37
+ element.style.removeProperty(name);
38
+ }
71
39
  element.style.removeProperty("--coras-page-background");
72
40
  element
73
41
  .querySelectorAll(":scope > [data-coras-fontfaces]")