@forgebuild/sidon-booking-widget
v0.4.17
Published
Drop-in berth booking widget (custom element) backed by sidon-ai's public marina API.
Readme
@forgebuild/sidon-booking-widget
A drop-in berth booking widget for sidon-ai marinas. Ships as <sidon-booking-widget> — a
self-contained custom element
compiled from Svelte, so it works on any site (Astro, plain HTML, React, WordPress — anything)
with one script include and one HTML tag. No framework integration required in the consumer.
It ports sidon-ai's Book*.astro wizard (document upload with AI-extracted autofill → sailor/boat
details + contract + Stripe payment hold → confirmation) into a portable package, talking directly
to sidon-ai's public marina API (the same cross-origin pattern portals already uses via
SIDON_ORIGIN).
Usage
<script type="module" src="/path/to/sidon-booking-widget.js"></script>
<sidon-booking-widget
marina="org_3D8ZaTo2ZvmMVRjcHgNeMBC7bIq"
api-origin="https://sidonmarine.uk"
from="2026-08-02"
to="2026-08-03"
length="25"
berth="B32"
locale="it"
></sidon-booking-widget>Or as an npm dependency (once published to npm — see below):
import '@forgebuild/sidon-booking-widget';then use <sidon-booking-widget> directly in markup — in Astro this needs no
@astrojs/svelte integration, since a custom element is just an HTML tag.
Attributes
| Attribute | Required | Description |
|---|---|---|
| marina | yes | The marina's ownerId (e.g. org_...) — not the URL slug. Read it off the live booking page's network requests if you don't already have it (content markdown copies of this value have gone stale before — verify against a live request, don't trust it blindly). |
| api-origin | no | Defaults to https://sidonmarine.uk. Override for a staging/dev sidon-ai instance. |
| from / to | no | Initial YYYY-MM-DD date range. Falls back to today → +3 nights. |
| length | no | Initial boat length in metres. Defaults to 15. |
| berth | no | Pre-selected berth id. If omitted (or no longer fits/available), the widget auto-picks the smallest available berth that fits the current length. |
| locale | no | it / en / fr / es. Defaults to en. |
| currency-symbol | no | Defaults to €. |
| variant | no | navy / dark. Defaults to navy. See Theming. |
The widget does not rewrite the host page's URL to stay in sync with its internal state (unlike sidon-ai's own book.astro) — it can't assume it owns the page it's embedded in. Listen for the events below if the host page wants to reflect state itself.
Events
Dispatched on the <sidon-booking-widget> element (bubbles: true, composed: true, so they cross
the shadow boundary and can be caught anywhere on the page):
sidon-step-change—{ step: 1 | 2 | 3 }, fired whenever the active wizard step changes.sidon-loa-change—{ loa: number }, fired on mount and whenever the sailor adjusts boat length in Summary's +/- control. Lets a host page (e.g. a map) grey out berths that no longer fit.sidon-booked—{ bookingId, marina }, fired once a booking is successfully submitted.sidon-booking-status—{ bookingId, status: 'confirmed' | 'cancelled' }, fired on step 3 if the marina confirms/declines the booking while the sailor still has the widget open (live, via Ably — see the caveat below). Never fires if the tab is closed before the marina acts.
Theming
The widget renders inside a Shadow DOM with its own compiled CSS, so it never depends on or collides with the host page's stylesheet. Brand colours cross the shadow boundary via inherited CSS custom properties — the same convention sidon-ai already uses to re-skin Pirates Bight:
sidon-booking-widget {
--brand-navy: #0e3a3e;
--brand-gold: #d49a3c;
--brand-ink: #0e3a3e; /* dark card surface */
}Falls back to sidon-ai's defaults (#16365b navy, #c9a86b gold) when unset.
Colour custom properties re-skin the palette; the variant attribute switches the Next button and
document-upload rows between sidon-ai's two button conventions (same mechanism as
BookStepNav/BookStep1Documents's own variant prop — kept in sync with those on purpose):
<sidon-booking-widget variant="dark" ...></sidon-booking-widget>navy(default, Positano) — gold Next button, navy-on-hover, rounded corners.dark(Fox/Pevero) — solid--brand-inkNext button, gold-on-hover, flush corners.
Live booking status (Ably)
Step 3 subscribes to sidon-ai's booking:<id> Ably channel (same mechanism as
book-success-live.ts) and repaints the panel in place — gold "pending" → green "confirmed" / red
"cancelled" — the moment the marina acts, without a page reload. ably is dynamically imported only
when step 3 is reached, but note the current build inlines all dynamic imports into the single
output file (see vite.config.ts), so it does add to every consumer's download regardless of
whether they ever reach step 3. Fails open on any error — the sailor just keeps the static "pending"
state, exactly like before this existed.
Known gap: sidon-ai's /api/marina/public/[marinaId]/* endpoints have CORS enabled
(src/lib/cors.ts) so this widget can call them from any embedding origin, but
/api/ably/token currently does not — a cross-origin token request 404s on preflight. Live status
updates work today only when the widget is embedded on a sidon-ai-served page; on a marina's own
domain (the actual npm/script-tag use case) the token fetch fails, is swallowed by the fail-open
error handling, and the panel silently stays in the static "pending" state. Needs a fix on the
sidon-ai side (add preflight/corsJson from lib/cors.ts to src/pages/api/ably/token.ts,
matching the other public endpoints) before this is live for real embeds.
What's deliberately different from sidon-ai's own wizard (v1 scope)
- No resend-confirmation-email on the confirmation step.
- No season-gated date picker / berth map integration — this widget doesn't ship a map. Berth
auto-pick is length + availability only, same math as sidon-ai's
recalc(), minus the map-driven manual-pick override. - Plain CSS, not Tailwind — kept the build dependency-free; visuals are a close but not pixel-exact port (no Teko/Hind webfonts bundled — system font stack instead).
- UI scope is the full-page wizard only (
Book*.astro, matches the/services/bookflow) — notportals' map-embeddedBerthsSizeBarpopover, which was a different, more tightly-coupled UI left for a future package version. That version now exists as<sidon-berth-map>— see below.
<sidon-berth-map>
A second custom element in this package: a self-contained berth map (buoy circles or SVG pontoon
overlays) with a floating quick-book card (dates, boat length, selected berth, submit). Ports
sea-company's BerthsGoogleMap.astro/berth-map-google.ts + BerthsBookingCard.astro/
berths-booking-card.ts — previously coordinated via a window.sidonBerthMap global — into one
globals-free widget, same "one drop-in tag" model as <sidon-booking-widget>.
Usage
import '@forgebuild/sidon-booking-widget/berth-map';<sidon-berth-map
marina="org_3D8ZaTo2ZvmMVRjcHgNeMBC7bIq"
maps-api-key="YOUR_GOOGLE_MAPS_JS_API_KEY"
api-origin="https://sidonmarine.uk"
map-style="chart"
locale="en"
book-path="/services/book"
></sidon-berth-map>Attributes
| Attribute | Required | Description |
|---|---|---|
| marina | yes | Same ownerId convention as <sidon-booking-widget> |
| maps-api-key | yes | Google Maps JS API key — this element's only extra credential |
| api-origin | no | Defaults to https://sidonmarine.uk |
| map-style | no | chart (buoy circles, default) or coastal (SVG pontoon overlays) |
| locale / currency-symbol | no | Same as <sidon-booking-widget> |
| from / to / length / berth | no | Same initial-state convention as <sidon-booking-widget> |
| book-path | no | If set, auto-navigates here with from/to/length/berth query params on submit — unless a sidon-quickbook-submit listener calls preventDefault(). Omit it and the element only fires the event; the host handles routing itself. |
| logo-mark / marina-name | no | Centre-pin logo for this widget's own marina (symmetric with otherMarinas below) |
| eyebrow / card-title / intro | no | Optional heading copy rendered above the quick-book card's form fields (eyebrow line, title, intro paragraph). Omit any of the three to skip that line — there's no default text, unlike the labels below. card-title (not title) to avoid colliding with the native HTML title attribute's browser tooltip. |
| range-label / range-placeholder / length-label / selected-label / selected-placeholder / cta-label | no | Override the quick-book card's copy (defaults: "Arrival – Departure", "", "Boat length (m)", "Selected buoy", "Tap a buoy", "Book a mooring") |
otherMarinas (JS property)
Peer-marina logo pins — set as a JS property, not a flat HTML attribute, since it's structured data:
document.querySelector('sidon-berth-map').otherMarinas = [
{ name: 'Pevero Mooring', lat: 41.117, lng: 9.394, logoMark: 'https://.../pevero-mark.svg', href: '/m/pevero-mooring/berths' },
];href is always host-supplied — the package owns no site's routing convention, so it never builds
peer-marina links itself.
Events
Same sidon-*, { bubbles: true, composed: true } convention as <sidon-booking-widget>:
sidon-berth-selected—{ berthId: string | null }sidon-loa-change—{ loa: number }(same name/shape<sidon-booking-widget>already uses, so one listener on a host page covers either element)sidon-quickbook-submit—{ from, to, length, berth }, dispatchedcancelable: true
Theming
Same --brand-navy / --brand-gold / --brand-ink CSS custom properties as <sidon-booking-widget>
— one theme block styles both elements consistently.
What's different from sea-company's original map + card
- No shared globals — doesn't read or write
window.sidonBerthMap; all state lives in the element itself, exposed only via attributes/properties/events. - No
#book-stepperselection lock — that assumed a shared page-level stepper from sea-company's/bookpage layout, which has no equivalent in a self-contained widget. - Plain CSS, not Tailwind, system font stack — not Teko/Hind — same dependency-free trade-off
as
<sidon-booking-widget>. - Submit navigates via URL params, not an in-place wizard hand-off — clicking "Book a mooring"
does not mount
<sidon-booking-widget>itself; it fires an event and/or navigates tobook-path.
Development
npm install
npm run dev # demo/ harness — hits sidon-ai's live public API directly
npm run build # library build → dist/sidon-booking-widget.js + dist/sidon-berth-map.js
# (each a single self-contained ES module, Svelte runtime bundled in —
# zero peer deps for consumers) + matching dist/*.d.ts for eachdemo/index.html renders three <sidon-booking-widget> instances (default theme, re-themed, and
variant="dark") plus one <sidon-berth-map> instance, all against Fox Mooring's real marina
data — useful for a quick visual/functional check without touching any consumer repo. The
berth-map instance needs a real Google Maps JS API key substituted in for local testing (don't
commit one).
Verified live (2026-08-02) against sidon-ai's production API: document upload, contract preview with live placeholder substitution, and Stripe's Payment Element mounting and initializing inside the widget's Shadow DOM all work — confirmed with Fox Mooring (real marina, test-mode Stripe key). Did not complete an actual booking submission, to avoid leaving a test record in a live dashboard.
Publishing (public npm)
Published to github.com/robooko/sidon-booking-widget (private repo) and the public npm registry as
@forgebuild/sidon-booking-widget. To publish a new version:
npm version <patch|minor|major>
npm publish # requires npm login as an account with publish rights on the @forgebuild scope
git push && git push --tagsnpm publish always rebuilds dist/ first via a prepublishOnly script — no need to run
npm run build separately, and no way to accidentally publish a stale build from an earlier
version (0.4.8 shipped this way before prepublishOnly existed: identical dist/ output to
0.4.7, silently missing that release's actual changes).
To install it as a dependency elsewhere: npm install @forgebuild/sidon-booking-widget. No registry
config or auth token needed — it's a public package.
