S/V MoonstruckCode reference

JavaScript reference

Generated from the JSDoc comments in the site's two scripts. The homepage's is assets/home.js; the three inner pages share assets/pages.js. Neither has a dependency; what the pages load is the minified copy tools/minify.py writes beside each one, so the comments here cost a visitor nothing. The data file beside them, assets/availability.js, is generated and is not documented here; ARCHITECTURE.md explains what it holds.

assets/home.js

The homepage is plain HTML, and this file is its only script. It has no dependencies, and it runs deferred, once the document has been parsed, inside one IIFE so that nothing leaks onto window. The inner pages have their own script, assets/pages.js, which mirrors several of the behaviors here (menu, reveals, floating bar, weather).

What the page actually loads is assets/home.min.js, the same code with the comments and the spacing taken out by tools/minify.py. This file is the one to read and the one to edit; the minified copy is built from it and committed beside it. That is why the comments below can be as thorough as they like: not one byte of them reaches a visitor.

The page reads without it. What script adds: the entrances, the nav's two states, the menu, the gallery and its two dialogs, the availability calendar, the footer weather and the floating action bar. The homepage's stylesheet has a "Without script" section for the few states that would otherwise wait for this file for ever.

Two parts of the page are written here, because they are made of data that is not in the markup:

the availability calendar from window.__availability (assets/availability.js, written by
                          tools/sync-availability.py) and today's date
the gallery's thumbnails  from GALLERY_COUNT

Each is copied from a template element that sits in index.html where its copies go, so the markup and its inline styles stay in the page and this file only fills them in. The footer weather comes from Open-Meteo.

State lives in the DOM (classes, data attributes, the hidden attribute, custom properties) so that the stylesheet can do the drawing; this file does not set a color or a transition. The hooks it writes:

nav.scrolled, nav.menu-open, .nav-burger.active     the nav's state, and the menu's
aria-current="location"                              the nav link for the section being read
hidden                                               the menu and its backdrop, the two dialogs, the open windows
data-revealed="yes"                                  an element that has entered
data-bg-on="yes"                                     a section whose background image may load
data-cal-day, data-lit, data-slow                    a calendar day's status, a lit window, the slow fade
data-zoomed, --zoom-natural                          the plate zoom's size
data-wx-faded, data-cta-shown                        the weather's fade, the floating bar
--cta-vv                                             the phone toolbar's height
data-settled="yes" on the root element               the page has loaded: in-page links scroll smoothly from here on
data-theme on the root element, aria-pressed         the theme that is on, and the footer switch's button for it

Doc comments follow JSDoc, plus one house tag, @section, which opens a named group; the stylesheets use the same tag. tools/build-docs.py turns them into docs/javascript.html.

Types

AvailabilityDatatypedef

Object · assets/home.js:50

window.__availability, as written to assets/availability.js by tools/sync-availability.py.

PropertyTypeDescription
sourcestringThe listing the blocks were read from.
asOfstringMonth the dates were last confirmed, as YYYY-MM.
first?stringFirst month worth drawing, as YYYY-MM, or null when there is nothing to show.
last?stringLast month worth drawing.
blocksArray<Array<string>>Runs of taken days, each [first day, last day, state], the days as YYYY-MM-DD and inclusive, the state one of "booked", "hold" or "unavailable". A day in no block is open.

Readingtypedef

Object · assets/home.js:61

One harbor's current conditions.

PropertyTypeDescription
namestringPlace label, as shown.
tempnumberTemperature in whole degrees Fahrenheit.
windnumberWind speed in whole knots.
codenumberWMO weather interpretation code, as Open-Meteo reports it.

Head and structured data

assets/home.js:75

The head in index.html is static. What it cannot know is the address the page is served from, so the absolute parts are written here.

injectSeo()function

assets/home.js:81

Writes what the static head cannot: a canonical link, absolute URLs for the social card images (crawlers want them absolute, and the source can only hold relative paths), and a JSON-LD graph of three nodes: the charter as a TouristTrip with its offers, the operator as an Organization, and the WebSite.

The offers are written by hand here, as the rate table's rows are in index.html, so a change of rates has to be made in both places.

Availability calendar

assets/home.js:154

From the blocks in assets/availability.js and today's date: each day's status, the open windows worth offering, and the months to draw. Dates are compared as YYYY-MM-DD strings, which sort the same way the days do. The sync script says only which days are taken; everything a visitor sees is worked out here. If that file does not load there are no months to draw, and the section goes on saying that the calendar is being brought up to date instead of showing every week as open.

availmember

AvailabilityData · assets/home.js:164

MIN_WINDOW_DAYSmember

assets/home.js:175

The owner's floor for a window worth offering: four calendar days as drawn in the grid.

blockOf(key)function

assets/home.js:178

The state of the block a day falls in, or "open".

ParameterTypeDescription
keystringThe day, as YYYY-MM-DD.

Returns string

statusOf(y, m, d)function

assets/home.js:188

A day's status as the grid draws it: "past", a block's state, "turnaround" or "open". The crew needs a clear day between charters, so an open day that touches a booking or a hold is that turnaround day: it reads as unavailable and is never offered. Neighbors are read from the blocks, not from this function, so a charter that ended yesterday still claims today.

ParameterTypeDescription
ynumberYear.
mnumberMonth, from zero.
dnumberDay of the month.

Returns string

STATUS_WORDSmember

assets/home.js:208

Each status in words, for the hidden text in every day: state is never carried by color alone.

openRuns()function

assets/home.js:211

The open windows: runs of at least MIN_WINDOW_DAYS open days, from today to the end of the last month drawn. They are what is left once the turnaround days are taken out, so a week between two charters lists as six days. Shorter runs stay in the grid but are not offered.

Returns Array<{a: Date, b: Date}> Each window's first and last day.

calDaysmember

HTMLElement[] · assets/home.js:235

Every day drawn, so that lighting a window is one pass over them.

paintWindow(slow)function

assets/home.js:243

Lights the days of the window that has the strongest claim, and no others.

ParameterTypeDescription
slowbooleanThe change is the ten-second pin running out, which fades over 1.2s; everything else answers at once.

pinWindow(i)function

assets/home.js:259

Choosing a window keeps its days inked for ten seconds and brings its month (or months) clear of the nav. One window at a time: choosing another replaces it. When the ten seconds are up the pointer's and the keyboard's claim on the same window go too, so the fade happens even with the pointer resting on the link. The link's own mailto still runs; this only adds the wayfinding.

ParameterTypeDescription
inumberIndex of the window.

renderCalendar()function

assets/home.js:292

Draws the section: the "current as of" line, one grid per month from its template, and the open windows as links, each a mailto with its dates in the subject line.

Reveals

assets/home.js:359

Elements with the class .reveal enter once, when 15% of them is on screen and they are 60px clear of the viewport's bottom edge. Entering means data-revealed="yes"; the stylesheet owns the motion, including the slide-ins, the polaroid plates and the gallery stage's blur.

Deferred loading

assets/home.js:374

What is kept off the first paint's back: the section backgrounds.

Navigation

assets/home.js:391

The bar's two states, the mark under the link for the section being read, and the mobile menu.

onMenuKeys(e)function

assets/home.js:419

Keys while the mobile menu is open: Escape closes it, and Tab wraps from the burger (which stays on screen as the close control) through the drawer's links.

ParameterTypeDescription
eKeyboardEvent

Listens document#keydown

toggleMenu()function

assets/home.js:435

Opens the drawer if it is closed and closes it if it is open. Open or closed is the drawer's hidden attribute; nothing else remembers it. Kept in step with it: the backdrop, the classes the stylesheet keys on (nav.menu-open, .nav-burger.active), aria-expanded, the page's scroll lock, the focus trap, and focus itself, which goes to the first link on opening and back to the burger on closing.

Scroll state

assets/home.js:464

handleScroll()function

assets/home.js:474

Reads the scroll position and settles two things:

the nav          past 100px it takes .scrolled: the glass bar over the hero becomes the navy bar
the floating bar shows once the middle of the boat section has passed the middle of the viewport,
                 and hides while the closing section is in view (it has its own coral Book button)
                 or the availability calendar fills the middle of the viewport (the bar would
                 cover its third column, and the same three actions sit under the grid)

Each is written only when it changes. Called once at the start as well, for a page opened part-way down.

Floating action bar

assets/home.js:505

ctaShiftmember

assets/home.js:509

The last offset written to --cta-vv; zero is also what the stylesheet assumes before any write.

syncViewportOffset()function

assets/home.js:511

Keeps the bar on the visible bottom edge of a phone. A fixed element is laid out against the layout viewport, but a mobile browser's toolbar or keyboard shrinks the visual viewport inside it. The difference is written to --cta-vv on the root element, and the bar's bottom offset adds it.

Listens window#resize, VisualViewport#resize, VisualViewport#scroll

Overlays and focus

assets/home.js:537

The photo viewer and the plate zoom are real dialogs (role="dialog", aria-modal) that share one mechanism. Both are in the page, hidden, and carry data-overlay. Opening one remembers the element that had focus, stops the page scrolling behind it and lands focus on its Close button; closing returns focus to where it came from. Escape closes, Tab wraps inside. The mobile menu has the same trap, with the burger standing in as its close control.

overlaymember

?HTMLElement · assets/home.js:546

The overlay that is open, or null.

overlayReturnmember

?Element · assets/home.js:548

The element that had focus when it opened.

openOverlay(node)function

assets/home.js:553

Opens an overlay.

ParameterTypeDescription
nodeHTMLElementThe viewer or the zoom dialog.

closeOverlay()function

assets/home.js:567

Closes the open overlay and hands focus back. Its image is let go of, so the next opening does not begin by showing the last one.

onOverlayKeys(e)function

assets/home.js:585

Keys while an overlay is open: Escape closes it, the left and right arrows turn the photo in the viewer, and Tab wraps among the overlay's buttons.

ParameterTypeDescription
eKeyboardEvent

Listens document#keydown

Plate zoom

assets/home.js:613

Any element carrying data-zoom opens the zoom dialog on the file it names, under its own alt text. The image opens fitted to the window; a click enlarges it to 260% and the dialog scrolls to pan, and another click fits it again. The enlarged files carry their width in their names (-2052.webp), which is how the zoom knows never to enlarge past the source's own pixels.

setZoomed(zoomed)function

assets/home.js:623

Sets the zoom's size and words its hint for the pointer in use.

ParameterTypeDescription
zoomedbooleanEnlarged, not fitted.

Theme

assets/home.js:864

The footer's switch between the site's three themes: Night Watch, which is the stylesheet's own tokens; Ensign, which is the [data-theme="ensign"] block beside them; and Reader, which is the stylesheet's last section and rolls the design back to the browser's own. The theme is the root element's data-theme attribute and nothing else, so the stylesheet does all the drawing.

The choice is kept in localStorage under "moonstruck-theme" and never leaves the browser. It is put on before first paint by a one-line script in the head, because this file is deferred and the page would otherwise be drawn in Night Watch first; what is here shows the switch (it ships hidden, since without script it would do nothing), keeps its pressed state in step, and remembers a change. assets/pages.js carries the same thing for the inner pages.

themeSwitchmember

?HTMLElement · assets/home.js:878

The switch in the footer.

themeMetamember

?HTMLMetaElement · assets/home.js:880

The meta tag that colors the browser's own chrome.

syncTheme()function

assets/home.js:882

Bring everything that follows the theme into line with the root element's attribute: each button's pressed state, and the browser's theme color, which is read back from the body's computed background so that no color is written here. The computed value, not the --ink-page token, because Reader's field is the system color Canvas, and only the computed value says what that is today.

Arriving with a fragment

assets/home.js:913

index.html#rates from an inner page's nav, or a shared link. The browser makes the jump itself as soon as it finds the target, but the log, the calendar and the thumbnails are written after that and push the target down, and so do images arriving above it; Safari does not anchor the scroll. So the jump is made again here, last of all, and once more at window load.

Smooth scrolling is switched on only after that (data-settled on the root element, which the stylesheet reads). A smooth scroll begun by the browser on arrival would still be traveling to where the target used to be, and would carry the page past every correction made here.

scrollToFragment()function

assets/home.js:926

Instant, not smooth: this is arriving, not traveling. scrollIntoView() honors the sections' scroll-margin-top, so the landing clears the bar exactly as a nav link's does.

settle()function

assets/home.js:937

The page has loaded: the last correction, then smooth scrolling from here on.

Listens window#load

assets/pages.js

The inner pages are plain HTML written by tools/chart-pages.py and styled by assets/pages.css. This file is their only script. It has no dependencies, and it is written in ES5 inside one IIFE so that nothing leaks onto window. The homepage does not load it: index.html has its own script, assets/home.js, which mirrors several of the behaviors here (menu, reveals, floating bar, weather).

What the pages actually load is assets/pages.min.js, the same code with the comments and the spacing taken out by tools/minify.py. This file is the one to read and the one to edit; the minified copy is built from it and committed beside it.

The split, the map dialog and the chart / log linking are progressive enhancements: without script a chart shows no divider, and each anchorage's map control is a plain link to Apple Maps. The entrances are not. .reveal blocks, pins and routes start hidden in pages.css and wait for this file to mark them as entered, so with script off they stay hidden.

Each block below looks its elements up first and does nothing when they are missing, so the same file serves all three pages (and the style guide) whatever subset of components a page has.

The contract with the markup is a small set of hooks, all emitted by tools/chart-pages.py:

.chart[data-split]        a chart that carries a satellite plate; the value is where the divider rests
.split-knob, .split-line  the divider's two draggable parts
[data-pin], [data-seg]    a numbered pin and a route segment inside a chart, keyed by leg or entry
[data-for][data-key]      a log entry or key row: names the chart id and the key it lights
.inset, .inset-open       an anchorage inset and the link that opens its satellite map
#mapbox                   the map dialog, present only when a MapKit token is set
.reveal, .gauges          elements that enter once when scrolled into view
.chart-scroll             the horizontal scroller around a chart, live at 900px and below, and at every width in Reader
.float-cta, .close        the floating action bar, and the closing section it hides over
footer .weather           the footer conditions widget
[data-theme-switch], [data-theme-pick]   the footer's theme switch and its buttons; the value is the theme's name, "" for Night Watch

State lives in the DOM (classes, data attributes, custom properties) so that pages.css can do the drawing; this file rarely sets a style directly. The exceptions are --split and --cta-vv, which are measurements only script can take.

Doc comments here follow JSDoc, plus one house tag, @section, which opens a named group; the stylesheets use the same tag. tools/build-docs.py turns both into docs/javascript.html and docs/css.html. The line-by-line comments are for reading straight through, and cost nothing on the wire: the minifier removes every one of them.

Static capture switch

assets/pages.js:47

Adding ?static to any inner page's URL resolves every entrance at once: reveals shown, routes drawn, pins placed, the chart / satellite divider already at rest. It exists for screenshots and print. The class is set here, synchronously and before anything else runs, because headless browsers capture at the load event, before a requestAnimationFrame chain or a timer can settle. pages.css carries the matching html.static rules.

Mobile menu

assets/pages.js:57

The burger (shown at 920px and below) opens #mobile-menu, a navy drawer from the right that is marked up as a modal dialog. It mirrors the homepage's menu: focus moves to the first link on opening and back to the burger on closing, Tab wraps inside the drawer, Escape or a click on the backdrop closes it, and the page behind does not scroll meanwhile. Open or closed is the drawer's data-open attribute ("yes" / "no"), which pages.css reads.

burgermember

?HTMLButtonElement · assets/pages.js:66

The burger button in the nav bar.

menumember

?HTMLElement · assets/pages.js:68

The drawer itself.

siteNavmember

?HTMLElement · assets/pages.js:70

The bar the burger sits in. While the drawer is open it rises above it, so the burger stays in reach.

menuBackdropmember

?HTMLElement · assets/pages.js:72

The scrim over the page behind the drawer. pages.css shows it from the drawer's data-open.

menuOpen()function

assets/pages.js:74

Whether the drawer is open. The attribute is the state; nothing else remembers it.

Returns boolean

onMenuKeys(e)function

assets/pages.js:79

Keyboard handling while the drawer is open: Escape closes it, and Tab is wrapped so focus cannot leave the dialog. The trap runs from the burger (which stays on screen as the close control) through every link in the drawer. Focus that has strayed outside the set is pulled back to the burger. Attached to document only while the drawer is open.

ParameterTypeDescription
eKeyboardEvent

Listens document#keydown

toggleMenu()function

assets/pages.js:97

Opens the drawer if it is closed and closes it if it is open, keeping these in step: the drawer's data-open (which also shows the backdrop), the bar's .menu-open class (which lifts the burger above the drawer as its close control), the burger's .active class (which turns its three waves into a cross), its aria-expanded, the page's scroll lock, and focus. The focus call is deferred a tick so the drawer is displayed before it receives focus; preventScroll keeps the page where it was.

Chart panning

assets/pages.js:128

At 900px and below a chart keeps a minimum width (--chart-min) and pans sideways inside its .chart-scroll box. It opens scrolled to its subject, not to the left edge: data-focus-x is the fraction of the chart's width to center. Applied at once and again at load, when the chart's final width is known. It runs before the split below, which rests its divider in the middle of whatever this leaves on screen.

Chart / satellite split

assets/pages.js:147

Every chart but the itineraries hero carries a satellite plate of exactly the same window, revealed by a draggable divider. The divider's position is the custom property --split on the chart, in percent from the left: chart to its left, satellite to its right. pages.css turns that one number into the plate's clip-path and the positions of the line and the knob.

Only the knob and the line take the drag, never the whole plate, because on a phone the plan charts already pan sideways and the page has to scroll past them (both set touch-action: pan-y).

Where it comes to rest is measured, not fixed. data-split is a percent of the whole chart, which is what it means while the whole chart is on screen; once the chart is wider than the screen and pans, that percent lands off to one side, so the divider rests at the middle of what is on screen instead and is within reach whatever the screen's size. The measurement is taken again whenever the geometry moves under it, unless the visitor has taken the divider somewhere.

A chart moves through four classes:

.split-on     script is running: the plate, line and knob are displayed (they are display: none without it)
.split-intro  the entrance is in flight: --split changes are transitioned, after --split-delay
.split-live   the line and knob are visible
.is-dragging  a pointer holds the divider

The knob is a role="slider". Its aria-valuenow is the divider's position; its aria-valuetext names the share of satellite, which is what a listener wants to know.

stillmember

boolean · assets/pages.js:174

True when nothing should animate: the ?static switch, or the visitor prefers reduced motion. Still pages open with the divider already at rest instead of playing the entrance.

splitsmember

Object<string, function(): void> · assets/pages.js:180

Entrance functions for the charts that animate, keyed by chart id. enter() calls one when its chart first scrolls into view, so the satellite wipes in after the route has drawn.

relaxersmember

Array<function(): void> · assets/pages.js:186

One per chart: puts its divider back where it should rest, after the geometry has moved. Called for every chart on the page at once, by relax() below.

restAt()function

assets/pages.js:203

Where the divider should rest. The authored rest is a percent of the whole chart, so it only says what it means while the whole chart is on screen; while the chart pans inside .chart-scroll the divider rests at the middle of the part that is on screen, whatever the screen's size.

Returns number Percent from the chart's left edge.

set(v)function

assets/pages.js:216

Moves the divider. The one place --split is written, so the custom property and the slider's ARIA values cannot disagree.

ParameterTypeDescription
vnumberPosition in percent from the chart's left edge; clamped to 0 to 100.

settle()function

assets/pages.js:228

Ends the entrance, so that later moves follow the pointer at once instead of easing over 1.5s.

at(e)function

assets/pages.js:230

The divider position a pointer event asks for, allowing for where the knob was taken.

ParameterTypeDescription
ePointerEvent

Returns number Percent from the left; the current value if the chart has no width yet.

down(e)function

assets/pages.js:236

Starts a drag from the knob or the line. Only the primary button counts. The pointer is captured so the drag carries on when it leaves the 40px knob; capture can throw for a synthetic event, which is harmless, hence the empty catch.

ParameterTypeDescription
ePointerEvent

Listens PointerEvent#pointerdown

move(e)function

assets/pages.js:252

Follows the pointer during a drag. Pointer events can arrive several times a frame, so only the newest position is kept and it is written once per animation frame.

ParameterTypeDescription
ePointerEvent

Listens PointerEvent#pointermove

up()function

assets/pages.js:263

Ends a pointer drag, however it ended: release, cancel, or losing the capture. It leaves the touch state below alone, because the cancel this fix is for arrives in the middle of a touch drag that is still going.

touchUp()function

assets/pages.js:269

Ends a touch drag: let the pointer drag go too, and forget the touch.

touchIdmember

assets/pages.js:271

A touch drag, held alongside the pointer one. Below 900px the chart sits in a .chart-scroll that pans sideways, and iOS decides whether that scroller has taken a sideways gesture before a pointer event can claim it, ignoring the touch-action: pan-y the knob and line declare: pointerdown fires, the scroller takes the gesture, and pointercancel ends the drag before it has moved. So the first move of a touch decides who owns the gesture -- more along than up is the divider's, and says so with preventDefault(), which no scroller argues with; more up than along is left to the page, so the full-height line does not trap vertical scrolling. While it is ours these events drive the divider themselves, and the drag survives the pointer stream being cancelled under it. touchId: the identifier of the touch being followed; owns: null until the axis is decided.

tracked(list)function

assets/pages.js:283

ParameterTypeDescription
listTouchList

Returns Touch|null The touch this drag follows, if it is still down.

touchDown(e)function

assets/pages.js:288

Takes note of a touch on the divider. Nothing is claimed yet: the first move decides.

ParameterTypeDescription
eTouchEvent

Listens TouchEvent#touchstart

touchMove(e)function

assets/pages.js:299

Follows a touch, deciding on the first move whose gesture it is.

ParameterTypeDescription
eTouchEvent

Listens TouchEvent#touchmove

note

assets/pages.js:348

Takes this chart's resting position again, after the geometry has moved under it: the chart's final width at load, a resize, a phone turned on its side. Nothing moves before the entrance has run or after the visitor has taken the divider, and a chart that does not pan measures the same authored rest and stays where it is.

splits[chart.id]()function

assets/pages.js:360

This chart's entrance: from closed (chart only), the satellite wipes in from the east and settles at rest.

relax()function

assets/pages.js:368

Takes every divider's resting position again, once per animation frame, when the geometry the measurement was made against has moved: the charts' final widths at load, a resize, a phone turned on its side. One listener for the page.

Listens Window#load, Window#resize

Anchorage maps

assets/pages.js:382

Every inset carries a link to a satellite map at its pin, and the whole plate answers for it: Apple Maps on an Apple device, Google Maps anywhere else (the link is re-aimed where the insets are wired). With a MapKit token on the page the click opens a live map in a dialog instead; without one, or if anything about the map fails, the link still goes to Apple Maps. Apple is not contacted before a click in either form: MapKit JS is fetched on the first one.

The token is MAPKIT_TOKEN in tools/chart-pages.py, which writes it into a meta tag and emits the #mapbox dialog only when it is set. While it is empty, which it is today, everything inside the if block below is dormant and the insets behave as plain links.

MapRequesttypedef

Object · assets/pages.js:395

What the dialog needs to draw one anchorage, read from the opener link's data attributes.

PropertyTypeDescription
latnumberLatitude of the pin: where the boat lies.
lonnumberLongitude of the pin.
dlatnumberHeight of the region to show, in degrees; about the inset's own window.
dlonnumberWidth of the region to show, in degrees.
nstringThe anchorage's number on the index chart, shown as the marker's glyph.
namestringThe anchorage's name.

mapMetamember

?HTMLMetaElement · assets/pages.js:406

The meta tag carrying the MapKit JS token, if the generator wrote one.

mapTokenmember

string · assets/pages.js:408

The token, or "" when the in-page map is off.

mapboxmember

?HTMLElement · assets/pages.js:410

The map dialog.

openMapmember

?function(HTMLAnchorElement): void · assets/pages.js:412

Opens the map dialog for an inset's link. Stays null when there is no token or no dialog, which is how the inset wiring below knows to leave the links alone.

mapFailed()function

assets/pages.js:428

Shows the fallback line over the map, unless MapKit is known to be working.

mapKeys(e)function

assets/pages.js:430

The dialog's keys: Escape closes, Tab wraps. The focusable set is rebuilt on every press because MapKit adds and removes its own controls, and an element counts only if it has a box on screen.

ParameterTypeDescription
eKeyboardEvent

Listens document#keydown

closeMap()function

assets/pages.js:446

Closes the dialog, lets the page scroll again, and returns focus to the link that opened it.

loadKit()function

assets/pages.js:454

Loads and initializes MapKit JS, once. Later calls return the same promise.

Returns Promise<Object> Resolves with window.mapkit; rejects if the script cannot load or init throws.

drawMap(mk)function

assets/pages.js:477

Aims the map at the wanted anchorage: satellite view, framed to about the inset's own window, with one marker in the pin's colors (Depth Cyan disc, Deep Water numeral).

ParameterTypeDescription
mkObjectThe mapkit namespace.

onApplemember

boolean · assets/pages.js:513

Whether this is an Apple device, where a maps.apple.com link opens the Maps app. Anywhere else (Android, Windows, Linux, ChromeOS) the insets go to Google Maps instead. An iPad asking for the desktop site calls itself a Mac, which is Apple either way.

Reveals, chart entrances and gauges

assets/pages.js:539

Three kinds of element enter once, when 15% of them is on screen and they are 60px clear of the viewport's bottom edge: .reveal blocks fade up, charts draw their route and place their pins, and the "how the week leans" gauges fill. Script only adds a class; pages.css owns the motion and its reduced-motion counterpart. Each element is unobserved after it has entered, so nothing replays.

enter(el)function

assets/pages.js:547

Marks one element as entered. Charts and gauges take .is-in; everything else takes .reveal-visible. A chart that registered a split entrance starts it here.

ParameterTypeDescription
elElement

iomember

?IntersectionObserver · assets/pages.js:556

The shared observer; null in a browser without IntersectionObserver, where everything enters at once.

Chart and log linking

assets/pages.js:568

Pointing at, or focusing, a log entry lights its leg and pin on the chart, and pointing at a pin lights its entry: the link runs both ways. A row names its chart with data-for and its key with data-key; pins carry data-pin and route segments data-seg with the same key (a leg number on the itineraries page, an anchorage number on the destination pages).

setActive(chartId, key)function

assets/pages.js:576

Lights one key on one chart, or clears the chart. Sets data-active on the chart, which is what dims the other routes and pins in pages.css, and toggles .active on the matching pin, route segment and rows.

ParameterTypeDescription
chartIdstringThe chart element's id.
key?stringThe key to light, or null to clear.

Floating action bar

assets/pages.js:609

Call, Email and Book, fixed to the viewport's right edge (its bottom edge on a phone). It shows once the hero has passed the middle of the viewport and hides over the closing section, which carries its own coral Book button: two corals never share a view (The One Coral Rule, DESIGN.md).

onScroll()function

assets/pages.js:619

Scroll handler, limited to one measurement per animation frame. Writes data-shown on the bar; pages.css fades it and takes it out of the pointer's way.

Listens window#scroll

ctaShiftmember

assets/pages.js:638

Keeps the bar on the visible bottom edge of a phone. A fixed element is laid out against the layout viewport, but a mobile browser's toolbar or keyboard shrinks the visual viewport inside it. The difference is written to --cta-vv on the root element, and the bar's bottom offset adds it.

Listens window#resize, VisualViewport#resize, VisualViewport#scroll

Theme

assets/pages.js:720

The footer's switch between the site's three themes: Night Watch, which is the stylesheet's own tokens; Ensign, which is the [data-theme="ensign"] block beside them; and Reader, which is the stylesheet's last section and rolls the design back to the browser's own. The theme is the root element's data-theme attribute and nothing else, so the stylesheet does all the drawing.

The choice is kept in localStorage under "moonstruck-theme" and never leaves the browser. It is put on before first paint by a one-line script in the head, because this file is deferred and the page would otherwise be drawn in Night Watch first; what is here shows the switch (it ships hidden, since without script it would do nothing), keeps its pressed state in step, and remembers a change. assets/home.js carries the same thing for the homepage.

themeSwitchmember

?HTMLElement · assets/pages.js:734

The switch in the footer.

themeMetamember

?HTMLMetaElement · assets/pages.js:736

The meta tag that colors the browser's own chrome.

syncTheme()function

assets/pages.js:738

Bring everything that follows the theme into line with the root element's attribute: each button's pressed state, and the browser's theme color, which is read back from the body's computed background so that no color is written here. The computed value, not the --ink-page token, because Reader's field is the system color Canvas, and only the computed value says what that is today.

Generated by tools/build-docs.py from the comments in the source. To change this page, change the comment and run it again.