@forgebuild/sidon-booking-widget 0.4.17 → 1.2.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/README.md +501 -164
- package/dist/astro-proxy.d.ts +44 -0
- package/dist/astro-proxy.js +205 -0
- package/dist/custom-element-berth-map.d.ts +1 -1
- package/dist/custom-element.d.ts +1 -1
- package/dist/elements.d.ts +118 -0
- package/dist/sidon-berth-map.js +1646 -1435
- package/dist/sidon-booking-widget.js +4497 -4580
- package/package.json +50 -49
- package/dist/favicon.svg +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** One OTA API key, or `{ [slug]: key }` for a site serving marinas on several properties. */
|
|
2
|
+
export type MarinaKeys = string | Record<string, string>;
|
|
3
|
+
export interface MarinaProxyOptions<C extends AstroLikeContext = AstroLikeContext> {
|
|
4
|
+
/** OTA origin, e.g. `https://ota.example.com`. */
|
|
5
|
+
otaBaseUrl: string;
|
|
6
|
+
/** Site rail: one key or slug → key. Give this or `auth`. */
|
|
7
|
+
keys?: MarinaKeys;
|
|
8
|
+
/** Staff rail: upstream credential headers for this request; null → 401 {error:'Not signed in'}. */
|
|
9
|
+
auth?: MarinaProxyAuth<C>;
|
|
10
|
+
/** Upstream timeout; Render cold starts can take most of a minute. */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface AstroLikeContext {
|
|
14
|
+
request: Request;
|
|
15
|
+
params: Record<string, string | undefined>;
|
|
16
|
+
readonly clientAddress: string;
|
|
17
|
+
}
|
|
18
|
+
export type AstroLikeRoute = (context: AstroLikeContext) => Promise<Response>;
|
|
19
|
+
export type MarinaProxyAuth<C extends AstroLikeContext = AstroLikeContext> = (context: C, slug: string) => Promise<Record<string, string> | null> | Record<string, string> | null;
|
|
20
|
+
export declare function createMarinaProxy<C extends AstroLikeContext = AstroLikeContext>({ otaBaseUrl, keys, auth, timeoutMs, }: MarinaProxyOptions<C>): (context: C) => Promise<Response>;
|
|
21
|
+
export interface MarinaClientOptions {
|
|
22
|
+
/** OTA origin, e.g. `https://ota.example.com`. */
|
|
23
|
+
otaBaseUrl: string;
|
|
24
|
+
keys: MarinaKeys;
|
|
25
|
+
/** Waits between 429 retries; injectable so the check doesn't really sleep. */
|
|
26
|
+
sleep?: (ms: number) => Promise<void>;
|
|
27
|
+
/** Per attempt, default 30000: a hung OTA must fail an SSR page, not hold it open. */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class MarinaClientError extends Error {
|
|
31
|
+
status: number;
|
|
32
|
+
constructor(message: string, status: number);
|
|
33
|
+
}
|
|
34
|
+
export declare function createMarinaClient({ otaBaseUrl, keys, sleep, timeoutMs, }: MarinaClientOptions): {
|
|
35
|
+
/** The configured slugs (`[]` for a single key): a multi-marina site's "other marinas". */
|
|
36
|
+
slugs: string[];
|
|
37
|
+
/** `GET /api/marina/guest/:slug[?from&to]`: the map payload, or null. */
|
|
38
|
+
getMarina: (slug: string, range?: {
|
|
39
|
+
from: string;
|
|
40
|
+
to: string;
|
|
41
|
+
}) => Promise<any>;
|
|
42
|
+
/** `GET /api/marina/guest/:slug/doc-requirements`: OTA's bare array, or null. */
|
|
43
|
+
getDocRequirements: (slug: string) => Promise<any[] | null>;
|
|
44
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Server-side half of the widget: an Astro route that relays the browser's
|
|
2
|
+
// /api/marina/guest/* calls to OTA with the site's API key (or, in the OTA
|
|
3
|
+
// dashboard, the signed-in staff member's credentials), so no credential
|
|
4
|
+
// ever reaches the page. Consumers mount it at
|
|
5
|
+
// src/pages/api/marina/guest/[...path].ts.
|
|
6
|
+
//
|
|
7
|
+
// Deliberately one self-contained file: tsc emits it to dist/ unbundled and
|
|
8
|
+
// Astro SSR loads it from node_modules, so a relative runtime import (which
|
|
9
|
+
// would be extensionless) or a node: import (edge runtimes) would break it.
|
|
10
|
+
// Erasable TS only, so scripts/check-astro-proxy.ts runs it under Node's
|
|
11
|
+
// type stripping with no build step. Never import the element entries here:
|
|
12
|
+
// they call customElements.define at import time, which throws on a server.
|
|
13
|
+
const GUEST_PREFIX = '/api/marina/guest/';
|
|
14
|
+
// Vercel's function body limit; OTA's own upload cap (4 MiB) is stricter.
|
|
15
|
+
const MAX_BODY_BYTES = 4_500_000;
|
|
16
|
+
const REQUEST_HEADERS = ['content-type', 'x-file-name', 'x-booking-token', 'user-agent'];
|
|
17
|
+
// Never content-encoding/content-length: Node's fetch has already decoded
|
|
18
|
+
// the body but keeps those headers. Never set-cookie or location.
|
|
19
|
+
const RESPONSE_HEADERS = ['content-type', 'cache-control', 'retry-after', 'ratelimit'];
|
|
20
|
+
// Astro has already percent-decoded params.path (repeatedly, in 6.4+), so a
|
|
21
|
+
// `%` left over, a backslash or a control character is an evasion attempt.
|
|
22
|
+
const BAD_SEGMENT = /[\\%\u0000-\u001f\u007f]/;
|
|
23
|
+
function jsonError(status, error) {
|
|
24
|
+
return new Response(JSON.stringify({ error }), {
|
|
25
|
+
status,
|
|
26
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Own-property lookup: a slug like `constructor` must not resolve to
|
|
30
|
+
// Object.prototype's function and be sent as a key.
|
|
31
|
+
function keyFor(keys, slug) {
|
|
32
|
+
if (typeof keys === 'string')
|
|
33
|
+
return keys || null;
|
|
34
|
+
return keys && Object.hasOwn(keys, slug) ? keys[slug] || null : null;
|
|
35
|
+
}
|
|
36
|
+
// Astro's getter throws (ClientAddressNotAvailable) on adapters without
|
|
37
|
+
// one; OTA then rate-limits the request in its per-property bucket.
|
|
38
|
+
function clientIp(context) {
|
|
39
|
+
try {
|
|
40
|
+
return context.clientAddress || null;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function createMarinaProxy({ otaBaseUrl, keys, auth, timeoutMs = 60_000, }) {
|
|
47
|
+
// Fail at startup: with both, which credential reaches OTA is a guess; with
|
|
48
|
+
// neither (e.g. OTA_API_KEY unset) every request would 404 like "no such marina".
|
|
49
|
+
if ((keys === undefined) === (auth === undefined)) {
|
|
50
|
+
throw new Error('createMarinaProxy: give exactly one of keys or auth');
|
|
51
|
+
}
|
|
52
|
+
return async (context) => {
|
|
53
|
+
const { request } = context;
|
|
54
|
+
// Astro dispatches HEAD to the GET export; OTA only needs to see GET.
|
|
55
|
+
const method = request.method === 'HEAD' ? 'GET' : request.method;
|
|
56
|
+
if (method !== 'GET' && method !== 'POST')
|
|
57
|
+
return jsonError(404, 'Not found');
|
|
58
|
+
const path = context.params.path ?? '';
|
|
59
|
+
const segments = path.split('/');
|
|
60
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..' || BAD_SEGMENT.test(s))) {
|
|
61
|
+
return jsonError(404, 'Not found');
|
|
62
|
+
}
|
|
63
|
+
// Belt and braces: whatever the guard missed, the resolved URL must still
|
|
64
|
+
// sit under the guest prefix — the API key and a staff bearer also open
|
|
65
|
+
// staff routes.
|
|
66
|
+
const url = new URL(GUEST_PREFIX + path, otaBaseUrl);
|
|
67
|
+
if (!url.pathname.startsWith(GUEST_PREFIX))
|
|
68
|
+
return jsonError(404, 'Not found');
|
|
69
|
+
url.search = new URL(request.url).search;
|
|
70
|
+
// After the path checks, so a rejected path never costs a token mint.
|
|
71
|
+
let credentials;
|
|
72
|
+
if (auth) {
|
|
73
|
+
const fromAuth = await auth(context, segments[0]);
|
|
74
|
+
if (!fromAuth)
|
|
75
|
+
return jsonError(401, 'Not signed in');
|
|
76
|
+
credentials = fromAuth;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const key = keyFor(keys, segments[0]);
|
|
80
|
+
if (!key)
|
|
81
|
+
return jsonError(404, 'Not found');
|
|
82
|
+
credentials = { 'x-api-key': key };
|
|
83
|
+
}
|
|
84
|
+
let body;
|
|
85
|
+
if (method === 'POST') {
|
|
86
|
+
if (Number(request.headers.get('content-length')) > MAX_BODY_BYTES) {
|
|
87
|
+
return jsonError(413, 'Request body too large');
|
|
88
|
+
}
|
|
89
|
+
// Buffered rather than streamed: Node's fetch rejects a stream body
|
|
90
|
+
// without duplex:'half', and a buffer gives OTA a Content-Length.
|
|
91
|
+
// ponytail: a chunked body is read fully before this check; Vercel caps
|
|
92
|
+
// it at 4.5 MB before we run, count bytes off the stream if self-hosted.
|
|
93
|
+
body = await request.arrayBuffer();
|
|
94
|
+
if (body.byteLength > MAX_BODY_BYTES)
|
|
95
|
+
return jsonError(413, 'Request body too large');
|
|
96
|
+
}
|
|
97
|
+
// Built from scratch, so cookies, Authorization, X-Forwarded-For and any
|
|
98
|
+
// inbound X-Client-Ip / X-Api-Key / X-Property-Id never reach OTA. The
|
|
99
|
+
// credentials go on after the allowlist so nothing inbound can shadow them.
|
|
100
|
+
const headers = new Headers();
|
|
101
|
+
for (const name of REQUEST_HEADERS) {
|
|
102
|
+
const value = request.headers.get(name);
|
|
103
|
+
if (value !== null)
|
|
104
|
+
headers.set(name, value);
|
|
105
|
+
}
|
|
106
|
+
for (const [name, value] of Object.entries(credentials))
|
|
107
|
+
headers.set(name, value);
|
|
108
|
+
const ip = clientIp(context);
|
|
109
|
+
if (ip)
|
|
110
|
+
headers.set('x-client-ip', ip);
|
|
111
|
+
let upstream;
|
|
112
|
+
try {
|
|
113
|
+
upstream = await fetch(url, {
|
|
114
|
+
method,
|
|
115
|
+
headers,
|
|
116
|
+
body,
|
|
117
|
+
redirect: 'manual',
|
|
118
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
// Caught here because a throw renders Astro's HTML 500 page, which the
|
|
123
|
+
// widget can't parse.
|
|
124
|
+
if (err?.name === 'TimeoutError')
|
|
125
|
+
return jsonError(504, 'Booking service timed out');
|
|
126
|
+
return jsonError(502, 'Booking service unavailable');
|
|
127
|
+
}
|
|
128
|
+
// OTA's guest routes never redirect; a 3xx is infrastructure (e.g. an
|
|
129
|
+
// http→https hop) and its Location must not reach the browser.
|
|
130
|
+
if (upstream.status >= 300 && upstream.status < 400)
|
|
131
|
+
return jsonError(502, 'Booking service unavailable');
|
|
132
|
+
const out = new Headers();
|
|
133
|
+
upstream.headers.forEach((value, name) => {
|
|
134
|
+
if (RESPONSE_HEADERS.includes(name) || name.startsWith('ratelimit-'))
|
|
135
|
+
out.set(name, value);
|
|
136
|
+
});
|
|
137
|
+
// Always a new Response: a fetch() Response's headers are immutable and
|
|
138
|
+
// Clerk's middleware appends to them, which would throw.
|
|
139
|
+
return new Response(upstream.body, { status: upstream.status, headers: out });
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export class MarinaClientError extends Error {
|
|
143
|
+
// A declared field, not a `public status` parameter property: those
|
|
144
|
+
// aren't erasable, and Node's type stripping runs this file as-is.
|
|
145
|
+
status;
|
|
146
|
+
constructor(message, status) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.name = 'MarinaClientError';
|
|
149
|
+
this.status = status;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Returns OTA's raw JSON (snake_case, NUMERIC columns as strings): each site
|
|
153
|
+
// maps the few fields it reads; the widget adapts its own copy. No cache:
|
|
154
|
+
// sites wrap this in theirs, and SSR availability must stay fresh.
|
|
155
|
+
export function createMarinaClient({ otaBaseUrl, keys, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), timeoutMs = 30_000, }) {
|
|
156
|
+
// Fail at startup: an unset OTA_API_KEY must not read as "no such marina".
|
|
157
|
+
if (!otaBaseUrl || !keys)
|
|
158
|
+
throw new Error('createMarinaClient: otaBaseUrl and keys are required');
|
|
159
|
+
// null = no such marina (OTA 404, or a slug outside the keys map), so a page can 404.
|
|
160
|
+
async function get(slug, suffix, query = '') {
|
|
161
|
+
const key = keyFor(keys, slug);
|
|
162
|
+
// '.' / '..' survive encodeURIComponent and would resolve out of /guest/, key attached.
|
|
163
|
+
if (!key || slug === '' || slug === '.' || slug === '..')
|
|
164
|
+
return null;
|
|
165
|
+
const url = new URL(GUEST_PREFIX + encodeURIComponent(slug) + suffix + query, otaBaseUrl);
|
|
166
|
+
// Build-time reads carry no X-Client-Ip, so a whole prerender shares one
|
|
167
|
+
// 100/min bucket per key: wait out a 429 rather than fail the build.
|
|
168
|
+
for (let retry = 0;; retry++) {
|
|
169
|
+
let res;
|
|
170
|
+
try {
|
|
171
|
+
res = await fetch(url, { headers: { 'x-api-key': key }, signal: AbortSignal.timeout(timeoutMs) });
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
// The forwarder's statuses, so a page handles both the same way; a
|
|
175
|
+
// raw DOMException / TypeError would otherwise escape as a 500.
|
|
176
|
+
if (err?.name === 'TimeoutError')
|
|
177
|
+
throw new MarinaClientError('Booking service timed out', 504);
|
|
178
|
+
throw new MarinaClientError('Booking service unavailable', 502);
|
|
179
|
+
}
|
|
180
|
+
if (res.status === 429 && retry < 3) {
|
|
181
|
+
await res.body?.cancel(); // free the connection while we wait
|
|
182
|
+
// NaN when absent or an HTTP-date; OTA's limiter sends seconds.
|
|
183
|
+
const secs = parseInt(res.headers.get('retry-after') ?? '', 10);
|
|
184
|
+
await sleep(Math.min(secs >= 0 ? secs : 5, 30) * 1000);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (res.status === 404)
|
|
188
|
+
return null;
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
// OTA errors are {error}; an edge / proxy error page may not be JSON at all.
|
|
191
|
+
const body = await res.json().catch(() => null);
|
|
192
|
+
throw new MarinaClientError(typeof body?.error === 'string' ? body.error : `OTA request failed (${res.status})`, res.status);
|
|
193
|
+
}
|
|
194
|
+
return res.json();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
/** The configured slugs (`[]` for a single key): a multi-marina site's "other marinas". */
|
|
199
|
+
slugs: typeof keys === 'string' ? [] : Object.keys(keys),
|
|
200
|
+
/** `GET /api/marina/guest/:slug[?from&to]`: the map payload, or null. */
|
|
201
|
+
getMarina: (slug, range) => get(slug, '', range ? `?${new URLSearchParams({ from: range.from, to: range.to })}` : ''),
|
|
202
|
+
/** `GET /api/marina/guest/:slug/doc-requirements`: OTA's bare array, or null. */
|
|
203
|
+
getDocRequirements: (slug) => get(slug, '/doc-requirements'),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './elements.js';
|
package/dist/custom-element.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './elements.js';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Hand-written typings for <sidon-booking-widget> and <sidon-berth-map>.
|
|
2
|
+
// The entry modules only register the elements (side-effect imports), so
|
|
3
|
+
// generated declarations come out as an empty `export {}`. The build copies
|
|
4
|
+
// this file verbatim to dist/elements.d.ts and both entry .d.ts files
|
|
5
|
+
// re-export it (scripts/write-element-types.mjs), which is why it imports
|
|
6
|
+
// nothing: a relative import would dangle in dist/.
|
|
7
|
+
//
|
|
8
|
+
// Keep it in step with the `props` blocks in BookingWidget.svelte and
|
|
9
|
+
// BerthMap.svelte and the events they emit. `npm run check` skips .d.ts
|
|
10
|
+
// files (skipLibCheck), so check an edit with:
|
|
11
|
+
// npx tsc --ignoreConfig --noEmit --skipLibCheck false --lib es2023,dom src/elements.d.ts
|
|
12
|
+
|
|
13
|
+
/** Peer-marina logo pin. Mirrors OtherMarina in lib/types.ts. */
|
|
14
|
+
export interface OtherMarina {
|
|
15
|
+
name: string;
|
|
16
|
+
lat: number;
|
|
17
|
+
lng: number;
|
|
18
|
+
logoMark: string | null;
|
|
19
|
+
/** Always host-supplied: the package owns no site's routing. */
|
|
20
|
+
href: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** OTA booking status. An expired booking reports `cancelled`. */
|
|
24
|
+
export type SidonBookingStatus = 'hold' | 'pending' | 'confirmed' | 'declined' | 'cancelled';
|
|
25
|
+
|
|
26
|
+
export interface SidonStepChangeDetail { step: 1 | 2 | 3 }
|
|
27
|
+
/** The booking widget fires this from the wizard only, never in booking-page mode. */
|
|
28
|
+
export interface SidonLoaChangeDetail { loa: number }
|
|
29
|
+
/** `bookingId` is OTA's booking UUID, `marina` the marina slug. */
|
|
30
|
+
export interface SidonBookedDetail { bookingId: string; marina: string }
|
|
31
|
+
export interface SidonBookingStatusDetail { bookingId: string; status: SidonBookingStatus }
|
|
32
|
+
/** A berth label, or null when deselected. */
|
|
33
|
+
export interface SidonBerthSelectedDetail { berthId: string | null }
|
|
34
|
+
/** Dates are `YYYY-MM-DD`; `berth` is a berth label. */
|
|
35
|
+
export interface SidonQuickbookSubmitDetail { from: string | null; to: string | null; length: number; berth: string | null }
|
|
36
|
+
|
|
37
|
+
// Element properties mirror the attributes, camelCased; optional like them.
|
|
38
|
+
export interface SidonBookingWidgetElement extends HTMLElement {
|
|
39
|
+
/** OTA marina slug. */
|
|
40
|
+
marina?: string;
|
|
41
|
+
/** '' (default) = this page's origin, where the forwarder route lives. */
|
|
42
|
+
apiOrigin?: string;
|
|
43
|
+
from?: string;
|
|
44
|
+
to?: string;
|
|
45
|
+
length?: number;
|
|
46
|
+
berth?: string;
|
|
47
|
+
/** en/it/fr/es (first two letters). Unset: the wizard is English and the
|
|
48
|
+
booking page follows the booking's own locale. */
|
|
49
|
+
locale?: string;
|
|
50
|
+
currencySymbol?: string;
|
|
51
|
+
variant?: 'navy' | 'dark';
|
|
52
|
+
/** Present (even empty) = booking-page mode; a value overrides `?booking=`. */
|
|
53
|
+
bookingId?: string | null;
|
|
54
|
+
mapsApiKey?: string;
|
|
55
|
+
mapStyle?: 'chart' | 'coastal';
|
|
56
|
+
mapHeight?: string;
|
|
57
|
+
mapWidth?: string;
|
|
58
|
+
mapGap?: string;
|
|
59
|
+
width?: string;
|
|
60
|
+
logoMark?: string | null;
|
|
61
|
+
marinaName?: string;
|
|
62
|
+
otherMarinas?: OtherMarina[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SidonBerthMapElement extends HTMLElement {
|
|
66
|
+
/** OTA marina slug. */
|
|
67
|
+
marina?: string;
|
|
68
|
+
mapsApiKey?: string;
|
|
69
|
+
/** '' (default) = this page's origin, where the forwarder route lives. */
|
|
70
|
+
apiOrigin?: string;
|
|
71
|
+
mapStyle?: 'chart' | 'coastal';
|
|
72
|
+
locale?: string;
|
|
73
|
+
currencySymbol?: string;
|
|
74
|
+
from?: string;
|
|
75
|
+
to?: string;
|
|
76
|
+
length?: number;
|
|
77
|
+
berth?: string;
|
|
78
|
+
/** 'staff' (the OTA dashboard): every listed berth is clickable and fires
|
|
79
|
+
sidon-berth-selected, occupied berths are painted for [from, to), and
|
|
80
|
+
from/to follow their attributes. No card, availability or boat-length
|
|
81
|
+
checks. Default 'guest'. */
|
|
82
|
+
mode?: 'guest' | 'staff';
|
|
83
|
+
/** Re-reads the map payload. Undefined until the element has mounted
|
|
84
|
+
(a microtask after it is connected), so call `el.refresh?.()`. */
|
|
85
|
+
readonly refresh?: () => void;
|
|
86
|
+
bookPath?: string;
|
|
87
|
+
logoMark?: string | null;
|
|
88
|
+
marinaName?: string;
|
|
89
|
+
otherMarinas?: OtherMarina[];
|
|
90
|
+
mapHeight?: string;
|
|
91
|
+
showWeather?: boolean;
|
|
92
|
+
eyebrow?: string;
|
|
93
|
+
cardTitle?: string;
|
|
94
|
+
intro?: string;
|
|
95
|
+
rangeLabel?: string;
|
|
96
|
+
rangePlaceholder?: string;
|
|
97
|
+
lengthLabel?: string;
|
|
98
|
+
selectedLabel?: string;
|
|
99
|
+
selectedPlaceholder?: string;
|
|
100
|
+
ctaLabel?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
declare global {
|
|
104
|
+
interface HTMLElementTagNameMap {
|
|
105
|
+
'sidon-booking-widget': SidonBookingWidgetElement;
|
|
106
|
+
'sidon-berth-map': SidonBerthMapElement;
|
|
107
|
+
}
|
|
108
|
+
// Dispatched with bubbles + composed, so listeners on any ancestor
|
|
109
|
+
// element get the typed detail too.
|
|
110
|
+
interface HTMLElementEventMap {
|
|
111
|
+
'sidon-step-change': CustomEvent<SidonStepChangeDetail>;
|
|
112
|
+
'sidon-loa-change': CustomEvent<SidonLoaChangeDetail>;
|
|
113
|
+
'sidon-booked': CustomEvent<SidonBookedDetail>;
|
|
114
|
+
'sidon-booking-status': CustomEvent<SidonBookingStatusDetail>;
|
|
115
|
+
'sidon-berth-selected': CustomEvent<SidonBerthSelectedDetail>;
|
|
116
|
+
'sidon-quickbook-submit': CustomEvent<SidonQuickbookSubmitDetail>;
|
|
117
|
+
}
|
|
118
|
+
}
|