@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.
- package/CHANGELOG.md +142 -0
- package/README.md +8 -7
- package/dist/components/accordion/accordion-item/accordion-item.styles.js +1 -1
- package/dist/components/alert/alert.styles.js +14 -14
- package/dist/components/attraction-card/base/attraction-card.styles.js +7 -3
- package/dist/components/attraction-discovery/attraction-discovery.styles.js +1 -1
- package/dist/components/attraction-header/attraction-header.styles.js +1 -1
- package/dist/components/attraction-option/attraction-option.styles.js +4 -4
- package/dist/components/basket/basket.styles.js +27 -26
- package/dist/components/basket-empty/basket-empty.styles.js +1 -0
- package/dist/components/basket-item/basket-item.component.js +10 -4
- package/dist/components/basket-item/basket-item.styles.js +9 -3
- package/dist/components/basket-item-ticket/basket-item-ticket.styles.js +1 -3
- package/dist/components/basket-mobile/basket-mobile.styles.js +5 -5
- package/dist/components/better-carousel/better-carousel.styles.js +2 -0
- package/dist/components/bottom-drawer/bottom-drawer.styles.js +6 -4
- package/dist/components/button/button.component.d.ts +0 -1
- package/dist/components/button/button.component.js +1 -8
- package/dist/components/button/button.constants.d.ts +1 -1
- package/dist/components/button/button.constants.js +1 -1
- package/dist/components/button/button.styles.js +66 -39
- package/dist/components/calendar/base/calendar.styles.js +3 -5
- package/dist/components/calendar/multi/multi.styles.js +7 -0
- package/dist/components/category-tag/category-tag.styles.js +10 -7
- package/dist/components/checkbox/checkbox.styles.js +2 -2
- package/dist/components/chip/chip.styles.js +2 -2
- package/dist/components/collect-fida/collect-fida.component.js +5 -3
- package/dist/components/collect-fida/collect-fida.styles.js +9 -16
- package/dist/components/crypto-auth-modal/crypto-auth-modal.component.js +6 -4
- package/dist/components/crypto-auth-modal/crypto-auth-modal.styles.js +2 -2
- package/dist/components/dialog/dialog.styles.js +10 -17
- package/dist/components/drawer/drawer.component.js +6 -0
- package/dist/components/drawer/drawer.styles.js +17 -3
- package/dist/components/dropdown/dropdown.component.js +7 -0
- package/dist/components/dropdown/dropdown.styles.js +3 -6
- package/dist/components/expiration-timer/expiration-timer.styles.js +3 -3
- package/dist/components/explore-card/explore-card.styles.js +1 -1
- package/dist/components/filter-chip/filter-chip.styles.js +11 -9
- package/dist/components/filters-drawer/filters-drawer.component.js +1 -1
- package/dist/components/filters-drawer/filters-drawer.styles.js +6 -5
- package/dist/components/image/image.styles.js +3 -3
- package/dist/components/image-chip/image-chip.styles.js +1 -1
- package/dist/components/input-field/input-field.styles.js +22 -17
- package/dist/components/itinerary/itinerary-item/itinerary-item.styles.js +1 -1
- package/dist/components/joined-fields/joined-fields.styles.js +10 -6
- package/dist/components/loading-spinner/loading-spinner.styles.js +1 -1
- package/dist/components/location-image/location-image.component.d.ts +6 -0
- package/dist/components/location-image/location-image.component.js +15 -2
- package/dist/components/location-image/location-image.styles.js +1 -1
- package/dist/components/locations-results/locations-results.styles.js +4 -6
- package/dist/components/marketing-tag/marketing-tag.styles.js +1 -1
- package/dist/components/message-card/message-card.styles.js +6 -6
- package/dist/components/option-group/option-chip/option-chip.styles.js +4 -3
- package/dist/components/order-confirmation/order-confirmation-item/order-confirmation-item.styles.js +9 -0
- package/dist/components/order-confirmation/order-confirmation.component.js +4 -3
- package/dist/components/order-confirmation/order-confirmation.styles.js +1 -1
- package/dist/components/otp/otp.styles.js +6 -6
- package/dist/components/overlay/overlay.styles.js +5 -1
- package/dist/components/pay-in/pay-in.styles.js +2 -2
- package/dist/components/payment-fiat-method/payment-fiat-method.styles.js +20 -4
- package/dist/components/payment-option/payment-option.styles.js +10 -12
- package/dist/components/performance-time/performance-time.styles.js +2 -2
- package/dist/components/phone-country-code/phone-country-code.styles.js +1 -1
- package/dist/components/points/points.styles.js +1 -1
- package/dist/components/search-button/search-button.styles.js +15 -7
- package/dist/components/seated-selector/seated-selector.component.d.ts +2 -0
- package/dist/components/seated-selector/seated-selector.component.js +53 -10
- package/dist/components/seated-selector/seated-selector.styles.js +77 -14
- package/dist/components/seats-io/seats-io.component.d.ts +2 -0
- package/dist/components/seats-io/seats-io.component.js +25 -2
- package/dist/components/seats-io/seats-io.constants.d.ts +2 -1
- package/dist/components/seats-io/seats-io.constants.js +6 -1
- package/dist/components/seats-io/seats-io.transform.d.ts +8 -2
- package/dist/components/seats-io/seats-io.transform.js +12 -4
- package/dist/components/select-list/select-list.styles.js +1 -0
- package/dist/components/select-list/select-option/select-option.styles.js +3 -3
- package/dist/components/sort-dropdown/sort-dropdown.styles.js +1 -1
- package/dist/components/sort-list/sort-list.styles.js +1 -1
- package/dist/components/sport-options/sport-option/sport-option.styles.js +2 -2
- package/dist/components/star-rating/star-rating.component.d.ts +1 -0
- package/dist/components/star-rating/star-rating.component.js +19 -23
- package/dist/components/star-rating/star-rating.styles.js +32 -6
- package/dist/components/tab/tab.styles.js +1 -1
- package/dist/components/tabs/tabs.styles.js +1 -1
- package/dist/components/tag/tag.styles.js +7 -7
- package/dist/components/text-section/text-section.styles.js +2 -1
- package/dist/components/time-tag/time-tag.styles.js +1 -2
- package/dist/components/tooltip/tooltip.styles.js +3 -3
- package/dist/components/tour-ticket-type-selector/tour-ticket-type/tour-ticket-type.styles.js +2 -2
- package/dist/components/tour-ticket-type-selector/tour-ticket-type-selector.component.js +1 -1
- package/dist/components/tour-ticket-type-selector/tour-ticket-type-selector.styles.js +1 -10
- package/dist/components/transfer-modal/transfer-modal.component.js +3 -3
- package/dist/components/transfer-modal/transfer-modal.styles.js +6 -8
- package/dist/containers/basket/basket.component.d.ts +1 -1
- package/dist/containers/basket/basket.component.js +56 -25
- package/dist/containers/footer/footer.component.d.ts +2 -0
- package/dist/containers/footer/footer.component.js +8 -0
- package/dist/containers/footer/footer.styles.js +12 -0
- package/dist/containers/navbar/navbar.component.d.ts +6 -1
- package/dist/containers/navbar/navbar.component.js +41 -9
- package/dist/containers/navbar/navbar.styles.js +47 -13
- package/dist/contexts/basket/basket.provider.d.ts +1 -0
- package/dist/contexts/basket/basket.provider.js +31 -4
- package/dist/contexts/properties.context.d.ts +4 -4
- package/dist/core/api/middlewares/booking-session/booking-session.constants.d.ts +1 -0
- package/dist/core/api/middlewares/booking-session/booking-session.constants.js +1 -0
- package/dist/core/api/middlewares/booking-session/booking-session.js +18 -27
- package/dist/core/api/middlewares/booking-session/booking-session.utils.d.ts +4 -4
- package/dist/core/api/middlewares/booking-session/booking-session.utils.js +13 -10
- package/dist/core/api/queries.js +3 -2
- package/dist/core/color-scheme.d.ts +14 -0
- package/dist/core/color-scheme.js +48 -0
- package/dist/core/currency/constants.d.ts +1 -1
- package/dist/core/currency/constants.js +4 -3
- package/dist/core/i18n/date.js +20 -9
- package/dist/core/i18n/locale-codes.d.ts +2 -2
- package/dist/core/i18n/locale-codes.js +4 -0
- package/dist/core/images/constants.d.ts +0 -5
- package/dist/core/images/constants.js +0 -5
- package/dist/core/images/helpers.d.ts +8 -0
- package/dist/core/images/helpers.js +14 -6
- package/dist/core/images/srcset.js +2 -2
- package/dist/core/storybook/demo.d.ts +4 -0
- package/dist/core/storybook/demo.js +17 -0
- package/dist/core/types/locales.d.ts +7 -0
- package/dist/core/utils/datetime.js +10 -4
- package/dist/core/utils/locale.d.ts +8 -1
- package/dist/core/utils/locale.js +10 -0
- package/dist/helpers/helpers.d.ts +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/locales/ar-SA.d.ts +279 -0
- package/dist/locales/ar-SA.js +288 -0
- package/dist/locales/da-DK.d.ts +100 -82
- package/dist/locales/da-DK.js +104 -89
- package/dist/locales/de-DE.d.ts +100 -82
- package/dist/locales/de-DE.js +104 -89
- package/dist/locales/en-IE.d.ts +250 -232
- package/dist/locales/en-IE.js +254 -239
- package/dist/locales/en-US.d.ts +100 -82
- package/dist/locales/en-US.js +107 -92
- package/dist/locales/es-ES.d.ts +100 -82
- package/dist/locales/es-ES.js +104 -89
- package/dist/locales/fr-FR.d.ts +100 -82
- package/dist/locales/fr-FR.js +104 -89
- package/dist/locales/fr-MA.d.ts +100 -82
- package/dist/locales/fr-MA.js +104 -89
- package/dist/locales/is-IS.d.ts +100 -82
- package/dist/locales/is-IS.js +104 -89
- package/dist/locales/it-IT.d.ts +100 -82
- package/dist/locales/it-IT.js +104 -89
- package/dist/locales/ms-MY.d.ts +100 -82
- package/dist/locales/ms-MY.js +106 -91
- package/dist/locales/nb-NO.d.ts +100 -82
- package/dist/locales/nb-NO.js +104 -89
- package/dist/locales/pl-PL.d.ts +100 -82
- package/dist/locales/pl-PL.js +104 -89
- package/dist/locales/pt-PT.d.ts +100 -82
- package/dist/locales/pt-PT.js +104 -89
- package/dist/locales/sv-SE.d.ts +100 -82
- package/dist/locales/sv-SE.js +104 -89
- package/dist/locales/th-TH.d.ts +100 -82
- package/dist/locales/th-TH.js +104 -89
- package/dist/locales/tr-TR.d.ts +100 -82
- package/dist/locales/tr-TR.js +104 -89
- package/dist/locales/zh-CN.d.ts +100 -82
- package/dist/locales/zh-CN.js +105 -90
- package/dist/locales/zh-HK.d.ts +279 -0
- package/dist/locales/zh-HK.js +288 -0
- package/dist/mixins/coinbase.mixin.js +1 -1
- package/dist/mixins/error-modal.styles.js +2 -2
- package/dist/mixins/location-search/location-search.js +3 -3
- package/dist/mixins/location-search/location-search.styles.js +6 -5
- package/dist/sdk/chrome.d.ts +9 -3
- package/dist/sdk/chrome.js +21 -9
- package/dist/sdk/element-adapter.js +11 -8
- package/dist/sdk/mount.d.ts +3 -3
- package/dist/sdk/mount.js +153 -48
- package/dist/sdk/navigation-intent.js +3 -0
- package/dist/sdk/page-registry.d.ts +14 -2
- package/dist/sdk/page-registry.js +33 -13
- package/dist/sdk/theme.d.ts +2 -16
- package/dist/sdk/theme.js +12 -44
- package/dist/sdk/types.d.ts +59 -30
- package/dist/sdk/url.d.ts +24 -3
- package/dist/sdk/url.js +66 -27
- package/dist/sdk/validation.d.ts +0 -7
- package/dist/sdk/validation.js +104 -48
- package/dist/styles/contrast-fixtures.d.ts +0 -5
- package/dist/styles/contrast-fixtures.js +79 -9
- package/dist/styles/contrast.js +4 -3
- package/dist/styles/defaults/component.js +17 -1
- package/dist/styles/primitives.d.ts +0 -10
- package/dist/styles/primitives.js +2 -12
- package/dist/styles/tokens.d.ts +1 -1
- package/dist/styles/tokens.generated.d.ts +1 -1
- package/dist/styles/tokens.generated.js +1 -1
- package/dist/styles/tokens.metadata.d.ts +1 -1
- package/dist/styles/tokens.metadata.js +378 -90
- package/dist/views/details-page/details-page.component.d.ts +5 -5
- package/dist/views/details-page/details-page.component.js +77 -80
- package/dist/views/details-page/details-page.styles.js +64 -9
- package/dist/views/help-page/help-page.component.d.ts +1 -0
- package/dist/views/help-page/help-page.component.js +11 -4
- package/dist/views/help-page/help-page.styles.js +21 -8
- package/dist/views/landing-page/landing-page.component.d.ts +4 -4
- package/dist/views/landing-page/landing-page.component.js +65 -65
- package/dist/views/landing-page/landing-page.styles.js +70 -3
- package/dist/views/payment-page/partials/activity-details/activity-details.styles.js +10 -33
- package/dist/views/payment-page/partials/billing-address/billing-address.styles.js +4 -9
- package/dist/views/payment-page/partials/confirmation/confirmation.styles.js +6 -6
- package/dist/views/payment-page/partials/hotel-pickup-list/hotel-pickup-list.styles.js +4 -4
- package/dist/views/payment-page/partials/payment-type-selector/payment-type-selector.styles.js +1 -0
- package/dist/views/payment-page/partials/stripe/stripe.component.d.ts +1 -1
- package/dist/views/payment-page/partials/stripe/stripe.component.js +11 -12
- package/dist/views/payment-page/partials/stripe/stripe.constants.d.ts +2 -15
- package/dist/views/payment-page/partials/stripe/stripe.constants.js +4 -17
- package/dist/views/payment-page/partials/stripe/stripe.styles.js +16 -23
- package/dist/views/payment-page/partials/stripe/stripe.utils.d.ts +25 -14
- package/dist/views/payment-page/partials/stripe/stripe.utils.js +111 -62
- package/dist/views/payment-page/partials/terms-checkbox/terms-checkbox.styles.js +1 -1
- package/dist/views/payment-page/payment-page.component.d.ts +1 -0
- package/dist/views/payment-page/payment-page.component.js +19 -9
- package/dist/views/payment-page/payment-page.styles.js +37 -32
- package/dist/views/payment-page/payment-page.utils.d.ts +7 -0
- package/dist/views/payment-page/payment-page.utils.js +29 -14
- package/dist/views/payment-page/payment-page.validators.js +9 -3
- package/dist/views/payment-page/services/payment-api.service.d.ts +1 -1
- package/dist/views/search-page/search-page.component.d.ts +1 -0
- package/dist/views/search-page/search-page.component.js +12 -6
- package/dist/views/search-page/search-page.styles.js +24 -2
- package/dist/views/suggestion-widget/suggestion-widget.styles.js +1 -1
- package/package.json +63 -76
- package/dist/custom-elements.json +0 -37397
- package/dist/sdk/meta.d.ts +0 -7
- package/dist/sdk/meta.js +0 -9
- package/dist/styles/tokens.css +0 -359
package/dist/sdk/types.d.ts
CHANGED
|
@@ -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
|
|
61
|
-
*
|
|
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" | "
|
|
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
|
-
|
|
161
|
-
page:
|
|
162
|
-
params?:
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
readonly
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
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<
|
|
49
|
+
reservedSegments?: Partial<Record<CorasRoutablePage, string>>;
|
|
29
50
|
};
|
|
30
51
|
export declare const DEFAULT_URL_STRATEGY: Required<Omit<CorasUrlStrategy, "reservedSegments">> & {
|
|
31
|
-
reservedSegments: Partial<Record<
|
|
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,
|
|
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
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return 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] =
|
|
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
|
|
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
|
-
|
|
136
|
-
|
|
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:
|
|
204
|
+
params: input.params ?? {},
|
|
166
205
|
...(input.locale
|
|
167
206
|
? { locale: normalizeLocale(input.locale) ?? input.locale }
|
|
168
207
|
: {}),
|
package/dist/sdk/validation.d.ts
CHANGED
|
@@ -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`). */
|
package/dist/sdk/validation.js
CHANGED
|
@@ -52,15 +52,31 @@ export const PAGE_PARAM_KEYS = {
|
|
|
52
52
|
"suggestion-widget": ["limit", "showExploreCard", "country", "city"],
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
-
|
|
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`),
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
|
|
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
|
-
"
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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(
|
|
295
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
};
|