@v-office/website-headless 0.1.1 → 1.0.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.
@@ -1,184 +1,505 @@
1
- //#region src/enhance/carousel.d.ts
2
- /**
3
- * Per-card image carousel: drives the prev/next chevrons by calling
4
- * `scrollTo` on the natively-scrolling image track (overflow-x: auto
5
- * with scroll-snap on the track on touch devices, or scroll-snap
6
- * none + JS-driven scrollTo on hover-capable devices).
7
- *
8
- * Native scroll matters for two reasons:
9
- * 1. Lazy-loaded `<img loading="lazy">` only triggers load when
10
- * the image enters the visible viewport via real scroll. With
11
- * transform-based shifting, the browser doesn't recognize the
12
- * image as "in view", so subsequent images stay un-loaded
13
- * (white squares) when the user clicks next.
14
- * 2. The scroll-snap behavior native scroll provides on touch is
15
- * what the iOS swipe momentum/snap relies on; transform-based
16
- * shifting can't reproduce it.
17
- *
18
- * Dots are optional and only updated when present. Their accessible label
19
- * follows `data-label-template` on the dots list (`{count}` images,
20
- * `{current}` shown) — the component renders the template from its copy, so
21
- * the enhancer carries no language of its own.
22
- */
23
- declare function initImageCarousels(selector?: string): void;
24
- declare function initRentalCarousel(viewport: HTMLElement): void;
1
+ import { An as LngLat, Mn as MapPoint, _a as EnhancePart, a as PlaceMarker, ba as GateMarkers, ga as ENHANCE_PARTS, jn as MapBounds, s as ClusterOptions, va as EnhanceParts, wn as SameSpotLayout, ya as GATE_MARKERS } from "../index-CPMXjfFH.mjs";
2
+ //#region src/enhance/lifecycle.d.ts
3
+ /** Removes everything a binder added. Safe to call more than once. */
4
+ type Dispose = () => void;
25
5
  //#endregion
26
- //#region src/enhance/overlay.d.ts
27
- /**
28
- * The rental page's history-integrated overlay — one controller, two callers.
29
- *
30
- * WHY IT EXISTS. The amenities modal and the description modal each carried
31
- * their own copy of this, and the two were identical: diffed after renaming the
32
- * modal noun, they differed in line wrapping and nothing else. Roughly seventy
33
- * lines of history handling, scroll locking, ESC and focus written twice, which
34
- * is two places for the same behaviour to be corrected in and one of them to be
35
- * forgotten.
36
- *
37
- * WHAT IT DOES NOT DECIDE. Nothing visual. The card, the backdrop, the header
38
- * bar and the scroll region are the component's; this file opens and closes
39
- * them. It also states no selectors of its own — every one is passed in, so the
40
- * markup and the string that finds it stay in the same file and a grep for
41
- * `data-amenities-overlay` still lands on the component that owns it.
42
- *
43
- * WHY `hidden` AND NOT `<dialog>`. This controller inherited the mechanism
44
- * rather than choosing it: every overlay on the page — the gallery lightbox,
45
- * the fullscreen viewer, the floor plan, the map — shows and hides by toggling
46
- * the `hidden` attribute, — see the view-transition helper below for why that stayed when
47
- * the fades went in. A second mechanism here would mean two ways to open a
48
- * layer on one page and two ways for the crossfade to have to work. Moving all
49
- * five onto `<dialog>` is a real question and a separate one; moving one is
50
- * not an improvement.
51
- *
52
- * WHY THE HISTORY ENTRY. A modal that swallows the back button is the single
53
- * worst thing a phone page can do: the gesture that means „go back" navigates
54
- * away from the rental instead of closing what is over it. Opening pushes one
55
- * state, and every close routes through `history.back()`, so the X, the ESC
56
- * key, a click beside the card, the browser's own button and the trackpad
57
- * swipe all take the same path and leave the history stack level.
58
- *
59
- * NOT ADOPTED BY THE OTHER TWO OVERLAYS on the rental page, and both for a stated
60
- * reason.
61
- * `RentalMap` is a fullscreen layer with no backdrop to click, no scroll region
62
- * to reset and unconditional focus on open; taking it would mean three option
63
- * flags to describe one caller, which is the API bending rather than the
64
- * callers converging. `RentalGallery`'s controller is `is:inline` and cannot
65
- * import anything at all — see the note in `gallery.ts`.
66
- */
67
- /** Run `mutate` inside a view transition where the browser has one and the
68
- * visitor has not asked for less motion; run it directly otherwise. */
69
- declare function withViewTransition(mutate: () => void): void;
70
- /** Install the page's single pair of modality listeners. Safe to call often. */
71
- declare function trackInputModality(): void;
72
- /** Focus `el` only when the last real input was a keyboard one. */
73
- declare function focusForKeyboard(el: HTMLElement | null): void;
74
- /** Every selector the controller needs, stated by the caller. Each is scoped to
75
- * `root`, so two overlays on one page cannot reach into each other. */
76
- interface HistoryOverlayOptions {
77
- /** The component's own root — the element that wraps BOTH the page-side
78
- * trigger and the overlay, so one lookup finds everything. */
79
- root: string;
80
- /** The dimmed backdrop. It carries the `hidden` toggle, and a click that
81
- * lands on it rather than on the card inside dismisses. */
82
- overlay: string;
83
- /** The inner scroll region, reset to the top on close. It is not the card:
84
- * the card is the clip and the header, the block inside it is the scroller. */
85
- scroller: string;
86
- /** The trigger on the page. */
87
- open: string;
88
- /** The X in the overlay's header. */
89
- close: string;
90
- /** Value stored on the pushed history entry, so a close can tell ITS OWN
91
- * state from someone else's before calling `back()`. Unique per overlay. */
92
- historyKey: string;
93
- }
94
- /** Bind one overlay. Returns silently when the markup is not on this page, so
95
- * a component whose overlay self-hides costs nothing. */
96
- declare function initHistoryOverlay(options: HistoryOverlayOptions): void;
6
+ //#region src/enhance/favorites.d.ts
7
+ /** Which Merkliste the hearts write to. */
8
+ interface FavoritesBinding {
9
+ /** `favoritesStorageKey(namespace)`: the site's one key (no default, so no guest loses a list). */
10
+ storageKey: string;
11
+ }
12
+ /** Bind every heart and badge under `root` (enhance() passes its root; late-rendered hearts are painted as
13
+ * they appear):
14
+ * - a heart is `[data-fav]` or `[data-part="favorite"]`; its rental id is `data-fav-id`, else the enclosing
15
+ * `[data-result-card]`'s value (a heart without an id does nothing). It gets `aria-pressed` and
16
+ * `data-saved`; every heart of one rental repaints together (a gallery's mobile and desktop copies);
17
+ * - a click toggles the rental, unless another handler already took the click (`defaultPrevented`: a React
18
+ * heart toggles the same store itself). Either way the pressed heart's glyph (its `svg`, else the button)
19
+ * gets `data-fav-burst="save|remove"` until its animation ends: a press is an event, so this is not CSS on
20
+ * `aria-pressed` (that would burst every saved heart on page load);
21
+ * - a badge is `[data-fav-badge]` or `[data-part="favorite-badge"]`: its text is the saved count, it
22
+ * carries `data-active="true"` while the count is above 0 and no `data-active` at 0 (hidden at 0: a
23
+ * "0" badge reads as broken; the kits wrote the value, so both `[data-active]` and
24
+ * `[data-active="true"]` match) and `data-fav-bump="on"` after each change; its `aria-label` is
25
+ * `data-template` ({count}), or `data-template-one` for one;
26
+ * - every change in the store, whoever made it (another tab included, through the `storage` event),
27
+ * dispatches FAVORITES_EVENT (`merkliste:changed`, `{ id, saved }`) on window for site scripts.
28
+ * Binding a root twice is a no-op. */
29
+ type BindFavorites = (root: ParentNode, options: FavoritesBinding) => Dispose;
30
+ /** See {@link BindFavorites}. */
31
+ export declare const bindFavorites: BindFavorites;
32
+ //#endregion
33
+ //#region src/enhance/share.d.ts
34
+ /** Where the shared link points. */
35
+ interface ShareOptions {
36
+ /** Canonical origin. Default: the button's `data-share-origin` (the site's configured origin), else the
37
+ * page's own origin. */
38
+ origin?: string;
39
+ /** Query parameters to drop besides the tracking parameters. */
40
+ strip?: readonly string[];
41
+ }
42
+ /** Bind one `[data-part="share"]` (or legacy `[data-share]`) button. The native share sheet opens on
43
+ * touch-primary devices only (a desktop visitor clicking "share" wants the link, not a system dialog);
44
+ * elsewhere the link goes to the clipboard, and where that is blocked a prompt shows it to copy by hand.
45
+ * A dismissed share sheet is a decision, not a failure: nothing else happens. After a copy the button
46
+ * carries `data-state="done"` for 2 s, its `[data-share-idle]` / `[data-share-done]` children swap
47
+ * (`hidden`), and the `[data-part="share-status"]` (or `[data-share-live]`) live region announces its
48
+ * `data-message` (else the done label's text). The title is `data-share-title`, else the document's.
49
+ * Binding twice is a no-op. */
50
+ type BindShare = (button: HTMLElement, options?: ShareOptions) => Dispose;
51
+ /** See {@link BindShare}. */
52
+ export declare const bindShare: BindShare;
53
+ //#endregion
54
+ //#region src/enhance/enhance.d.ts
55
+ /** Which engine families enhance() binds. Each is on by default; pass false where the site binds one
56
+ * itself. Favorites need the site's storage key and are off without it. */
57
+ interface EnhanceOptions {
58
+ /** `[data-part="strip"]` and the legacy `.card-images` strips (bound when the browser is idle: binding
59
+ * every row and card at once was a long task in front of the first photo's paint). */
60
+ strips?: boolean;
61
+ /** `[data-part="rental-gallery"]` galleries with their strips and layers. */
62
+ galleries?: boolean;
63
+ /** `<dialog data-part="layer">` layers (description, amenities, any site dialog on the contract). */
64
+ layers?: boolean;
65
+ /** `[data-part="reveal"]` "show more" lists. */
66
+ reveals?: boolean;
67
+ /** `[data-part="clamp"]` line-clamped texts. */
68
+ clamps?: boolean;
69
+ /** `[data-part="share"]` / `[data-share]` buttons; an object sets their options. */
70
+ share?: boolean | ShareOptions;
71
+ /** The lazy-photo fade inside every gallery. */
72
+ imageFade?: boolean;
73
+ /** `{ storageKey }` binds the `[data-fav]` hearts and badges below the root; false or absent skips. */
74
+ favorites?: false | FavoritesBinding;
75
+ }
76
+ /** Bind everything below `root` (default `document`). Idempotent: an element already bound (by an earlier
77
+ * call or a hydrated component's useEnhance) is left alone and stays its first binder's, so a second call
78
+ * binds only what is new and its Dispose unbinds only that. */
79
+ type Enhance = (root?: ParentNode, options?: EnhanceOptions) => Dispose;
80
+ /** See {@link Enhance}. */
81
+ export declare const enhance: Enhance;
82
+ //#endregion
83
+ //#region src/enhance/layers.d.ts
84
+ /** How one dialog behaves. */
85
+ interface LayerOptions {
86
+ /** The URL hash while open. Default: the dialog's id (one of the two is required; binding throws without). */
87
+ name?: string;
88
+ /** The inner scroller, reset to the top before the dialog closes (after closing it has no box, so a reset
89
+ * then is a silent no-op and the layer reopens where it was left). Default: its `[data-part="layer-body"]`. */
90
+ scroller?: HTMLElement | null;
91
+ /** Close on a click that lands on the dialog's own box, outside its card. Default true. */
92
+ closeOnBackdrop?: boolean;
93
+ /** After opening; receives the element that opened it (the gallery reads its photo index). */
94
+ onOpen?: (opener: HTMLElement | null) => void;
95
+ /** Before closing, while layout can still be read (the viewer's current photo). */
96
+ onClose?: () => void;
97
+ }
98
+ /** A bound dialog. */
99
+ interface LayerHandle {
100
+ /** Open (no-op when open), pushing the layer's history entry. */
101
+ open(opener?: HTMLElement | null): void;
102
+ /** Close through history.back() when the layer's entry is on top, else directly. */
103
+ close(): void;
104
+ /** Whether the layer is open. */
105
+ readonly isOpen: boolean;
106
+ /** Remove every listener; an open layer closes without touching history (the page is being left). */
107
+ dispose: Dispose;
108
+ }
109
+ /** Bind one dialog. Openers are `[data-part="layer-open"][aria-controls=<dialog id>]` anywhere in the
110
+ * document; closers are `[data-part="layer-close"]` inside it. Body scroll is locked while any layer is
111
+ * open; focus moves in (to the first closer) and back only after keyboard input. Binding a dialog twice
112
+ * returns the first handle. */
113
+ type BindLayer = (dialog: HTMLDialogElement, options?: LayerOptions) => LayerHandle;
114
+ /** See {@link BindLayer}. */
115
+ export declare const bindLayer: BindLayer;
116
+ //#endregion
117
+ //#region src/enhance/strip.d.ts
118
+ /** How a strip steps and ends. Each option may also come from the markup: `data-paging`, `data-edges`
119
+ * on the strip root; an option passed here wins over the markup. */
120
+ interface StripOptions {
121
+ /** "page": every item is the track's width (card photos, the mobile gallery, the viewer).
122
+ * "snap": items of any width, stepped to the nearest item start (rental rows, review cards). Default "page". */
123
+ paging?: "page" | "snap";
124
+ /** "stop": the arrows disable at the ends (the strip root carries `data-engaged` once first scrolled, so
125
+ * CSS can hide a disabled arrow until then and grey it in place after; a click cannot fall through to
126
+ * the card link where an arrow was). "wrap": next after the last returns to the first. Default "stop". */
127
+ edges?: "stop" | "wrap";
128
+ /** Mouse drag (2 px threshold) with a suppressor for the click that ends it. Default: paging === "snap". */
129
+ drag?: boolean;
130
+ /** Hand the next item its `data-src` / `data-srcset` on pointer, touch, focus or scroll intent, so photos
131
+ * 2–5 of a card are not fetched at page start (the browser's lazy-load distance applies sideways too).
132
+ * Default true. */
133
+ deferSlides?: boolean;
134
+ }
135
+ /** A bound strip. */
136
+ interface StripHandle {
137
+ /** The current item among the items that have a box. */
138
+ readonly index: number;
139
+ /** The number of items that have a box (a hidden tile does not count). */
140
+ readonly total: number;
141
+ /** Scroll to an item (clamped). "instant" overrides a CSS `scroll-behavior: smooth` (an assignment under
142
+ * smooth scrolling silently never moved a strip); "smooth" becomes instant under reduced motion.
143
+ * Default: "instant" for page strips, "smooth" for snap strips. A strip without a box ignores it. */
144
+ goTo(index: number, how?: "instant" | "smooth"): void;
145
+ /** One item back or forward, wrapping when `edges` is "wrap". */
146
+ step(delta: 1 | -1): void;
147
+ /** Back to the start (a tab switch swapped the items). */
148
+ reset(): void;
149
+ /** Remove every listener and observer. */
150
+ dispose: Dispose;
151
+ }
152
+ /** Bind one strip: `root` is a `[data-part="strip"]` (or the legacy `.card-images`) holding a
153
+ * `[data-part="strip-track"]` (legacy `[data-img-track]`), the horizontal scroller, and optional
154
+ * `strip-prev` / `strip-next` (`data-img-prev` / `data-img-next`), `strip-dots` (`data-dots`, one child
155
+ * per item) and `strip-counter`. Items are the track's `[data-part="strip-item"]`s, else its children.
156
+ * The engine writes state attributes only, never a class: `data-current` on the current item,
157
+ * `data-active` on its dot, `disabled` on the arrows, `data-engaged` / `data-dragging` on the root, the
158
+ * dots' `aria-label` and the counter's text from their `data-template` ({current}, {count}). Binding
159
+ * waits until the track has children; binding a strip twice returns the first handle. */
160
+ type BindStrip = (root: HTMLElement, options?: StripOptions) => StripHandle;
161
+ /** See {@link BindStrip}. */
162
+ export declare const bindStrip: BindStrip;
97
163
  //#endregion
98
164
  //#region src/enhance/gallery.d.ts
99
- /**
100
- * The gallery's layered overlays, as an engine the component configures.
101
- *
102
- * WHAT IT OWNS: opening and closing a stack of layers, the browser-history
103
- * entry each one pushes, the scroll lock, layered ESC, focus on open and
104
- * restore on close, the lazily-loaded photo reveal, the scroll-snap viewer's
105
- * index and its prev/next stepping, and keeping a page-level carousel on the
106
- * photo the visitor was last looking at.
107
- *
108
- * WHAT IT DECIDES: nothing visual. It states **no selector of its own** — every
109
- * one is passed in, the rule `overlay.ts` already set, so the markup and the
110
- * string that finds it stay in the same file and a grep for
111
- * `data-gallery-lightbox` still lands on the component that owns it. It sets no
112
- * class, reads no colour and renders no markup.
113
- *
114
- * WHY THE LAYERS ARE A LIST. The controller this replaces hardcoded its three
115
- * layers and their nesting order in three separate places — the `popstate`
116
- * handler, the `keydown` handler and the body-scroll restore — and opened with
117
- * `if (!lightbox || !lightboxClose || !fullscreen || !fsTrack) return`. A page
118
- * that opens the viewer straight from the photos, with no intermediate grid,
119
- * therefore got **no gallery at all** and no error saying so. That is not a
120
- * hypothetical composition: it is the obvious one for a site whose photos are
121
- * already a full-width strip. Layers are now whatever the caller lists,
122
- * outermost first, and a layer whose markup is absent is simply not bound.
123
- *
124
- * WHY IT IS A MODULE AND NOT `is:inline`. It was inline from the gallery's first
125
- * commit with no reason recorded, and the cost was stated in two places: the
126
- * four-line copy of `withViewTransition` it had to carry, and the third copy of
127
- * the input-modality listeners `overlay.ts` names in its own header. An
128
- * `is:inline` script is processed by neither Vite nor Astro and can import
129
- * nothing, so both copies were forced rather than chosen.
130
- *
131
- * The one thing that changes by importing: the emitted script crosses Vite's
132
- * `assetsInlineLimit` (4096 bytes), so Astro links it as an external module
133
- * instead of writing it into the page. Any gate that located MARKUP by
134
- * searching the built HTML was, until it read markup only, able to match the
135
- * selector literals in the inlined source — see the note in the blueprints'
136
- * `fact-check.py`. That is fixed there rather than worked around here.
137
- */
138
- /** One overlay the gallery can open. Absent markup → not bound, no error. */
139
- interface GalleryLayer {
140
- /** Stable name, used for this layer's history entry. */
141
- name: string;
142
- /** The panel that carries the `hidden` toggle. */
143
- panel: string;
144
- /** Everything that opens it. */
145
- open: string;
146
- /** The X inside it. */
147
- close?: string;
148
- /** Backdrop: a click that lands on it and not on the card inside dismisses. */
149
- backdrop?: string;
150
- /** Inner scroll region, reset to the top on close. Not the card — the card is
151
- * the clip and the header, the block inside it is the scroller. */
152
- scroller?: string;
153
- /** This layer is the photo viewer: its openers carry an image index and it
154
- * drives `track`. */
155
- opensAt?: "index";
156
- /** Preload the panel's image on the first hover/focus of any opener, so the
157
- * panel does not open onto an empty surface while the bytes arrive. */
158
- warmOnIntent?: boolean;
159
- /** Publish the panel image's real aspect ratio as `--fp-ar` once known, so a
160
- * wrong width/height guess does not size the box. */
161
- fitImage?: string;
162
- }
163
- interface GalleryOptions {
164
- /** The element wrapping every trigger and every layer, so one lookup finds
165
- * them all and two galleries on a page cannot reach into each other. */
166
- root: string;
167
- /** Horizontal scroll-snap track inside the viewer layer. */
168
- track?: string;
169
- trackPrev?: string;
170
- trackNext?: string;
171
- /** Element whose text becomes `n / total`. */
172
- counter?: string;
173
- /** A page-level carousel kept in sync with the viewer on close. */
174
- carousel?: string;
175
- carouselCounter?: string;
176
- /** Attribute on a viewer opener carrying its image index. */
177
- indexAttr?: string;
178
- /** Outermost first. */
179
- layers: GalleryLayer[];
180
- }
181
- /** Bind one gallery. Returns silently when its root is not on this page. */
182
- declare function initGallery(options: GalleryOptions): void;
165
+ /** Bind one `[data-part="rental-gallery"]`:
166
+ * - every strip inside it (the mobile strip, the viewer, paged floor plans) as a page strip;
167
+ * - its `<dialog>`s `rental-gallery-grid`, `rental-gallery-viewer`, `rental-floorplan` as layers;
168
+ * any element inside the gallery whose `aria-controls` names one of them opens it (mosaic tiles,
169
+ * "all photos", grid photos, strip photos, the floor-plan button); an opener of the viewer carries
170
+ * `data-index` (0-based photo), else the viewer opens on the photo the mobile strip shows;
171
+ * - the viewer lands on that photo once it has a width (up to 5 frames, never on photo 1 by accident),
172
+ * steps with the arrow keys, and on close the mobile strip returns to the photo last viewed;
173
+ * - the floor-plan images publish their natural ratio as `--fp-ar`, and the floor plan is warmed on the
174
+ * first hover or focus of its opener (srcset before src, so the warm-up fetches what the dialog shows).
175
+ * The per-photo caption and counter follow the photo, not the scroll event: the strip marks the photo
176
+ * more than half on screen with `data-current`. Binding twice is a no-op. */
177
+ type BindGallery = (root: HTMLElement) => Dispose;
178
+ /** See {@link BindGallery}. */
179
+ export declare const bindGallery: BindGallery;
180
+ //#endregion
181
+ //#region src/enhance/reveal.d.ts
182
+ /** How a reveal list grows. */
183
+ interface RevealOptions {
184
+ /** Items each "more" press reveals. Default: all that remain. The markup may say `data-step` on the root. */
185
+ step?: number;
186
+ /** A `reveal-less` control hides the revealed items again. Default: true when the markup renders a
187
+ * `[data-part="reveal-less"]` (a rendered control that could never work would be a dead button). */
188
+ collapsible?: boolean;
189
+ }
190
+ /** Bind one `[data-part="reveal"]`: its `[data-part="reveal-item"][hidden]` items are revealed by
191
+ * `[data-part="reveal-more"]`, which hides itself when nothing remains; `reveal-less` (shown once something
192
+ * is revealed) hides them again. The root carries `data-open` while revealed items show. Focus lands on the
193
+ * first revealed item for keyboard users. Binding twice is a no-op. */
194
+ type BindReveal = (root: HTMLElement, options?: RevealOptions) => Dispose;
195
+ /** Bind one `[data-part="clamp"]`: `[data-part="clamp-toggle"]` shows only while
196
+ * `[data-part="clamp-text"]` overflows its line clamp (re-measured on resize) and toggles `data-expanded`
197
+ * on the root and `aria-expanded` on itself; the clamp itself is CSS on the root's state. Binding twice is
198
+ * a no-op. */
199
+ type BindClamp = (root: HTMLElement) => Dispose;
200
+ /** See {@link BindReveal}. */
201
+ export declare const bindReveal: BindReveal;
202
+ /** See {@link BindClamp}. */
203
+ export declare const bindClamp: BindClamp;
204
+ //#endregion
205
+ //#region src/enhance/image-fade.d.ts
206
+ /** Bind one root (enhance() binds every `[data-part="rental-gallery"]`). The root carries `data-image-fade`
207
+ * while bound: that attribute arms the CSS that keeps an unmarked lazy image transparent, so without the
208
+ * engine every photo simply shows. A lazy image gets `data-loaded="fade"` when it lands, `"cache"` when it
209
+ * was already decoded at bind (nothing arrived, so nothing animates) or when it failed (a 404 must not stay
210
+ * invisible and take its alt text with it). One capture listener covers the photos inside closed layers,
211
+ * which only start fetching once their layer opens. Binding twice is a no-op. */
212
+ type BindImageFade = (root: HTMLElement) => Dispose;
213
+ /** See {@link BindImageFade}. */
214
+ export declare const bindImageFade: BindImageFade;
215
+ //#endregion
216
+ //#region src/enhance/motion.d.ts
217
+ /** Run `mutate` inside a view transition where the browser has one and the visitor has not asked for
218
+ * less motion; run it directly otherwise. The look (named groups, timing) is the site's stylesheet's.
219
+ * An aborted transition is expected (a hidden tab, a second transition) and swallowed: it says the
220
+ * fade did not happen, never that `mutate` did not run. */
221
+ type WithViewTransition = (mutate: () => void) => void;
222
+ /** Install the page's one pair of input-modality listeners (pointer vs keyboard). Reference-counted:
223
+ * every binder that moves focus claims it and releases it through the Dispose; the listeners go with
224
+ * the last release. */
225
+ type TrackInputModality = () => Dispose;
226
+ /** Focus `element` only when the last real input was the keyboard. Moving focus into a layer is right
227
+ * for a keyboard user and wrong for a thumb: iOS paints a ring on a tapped button after a programmatic
228
+ * focus, which `:focus-visible` cannot prevent. Needs `trackInputModality()` to have been claimed. */
229
+ type FocusForKeyboard = (element: HTMLElement) => void;
230
+ /** See {@link WithViewTransition}. */
231
+ export declare const withViewTransition: WithViewTransition;
232
+ /** See {@link TrackInputModality}. */
233
+ export declare const trackInputModality: TrackInputModality;
234
+ /** See {@link FocusForKeyboard}. */
235
+ export declare const focusForKeyboard: FocusForKeyboard;
236
+ //#endregion
237
+ //#region src/enhance/map/card-hover.d.ts
238
+ /** Delegated on the document, so it works with any grid (a site's own included) and survives every
239
+ * re-render: `onHover(id)` when the pointer or focus enters a `[data-result-card="<id>"]`, `onHover(null)`
240
+ * when it leaves every card or the window; called once per change. Focus counts like hover, so keyboard
241
+ * users get the same highlight. */
242
+ type ObserveResultCardHover = (onHover: (id: string | null) => void) => Dispose;
243
+ /** See {@link ObserveResultCardHover}. */
244
+ export declare const observeResultCardHover: ObserveResultCardHover;
245
+ //#endregion
246
+ //#region src/enhance/map/maplibre.d.ts
247
+ /** The library: its Map, Marker and Popup constructors. */
248
+ interface MapLibreLike {
249
+ /** `new maplibregl.Map(options)`. */
250
+ Map: new (options: Record<string, unknown> & {
251
+ container: HTMLElement;
252
+ }) => MapLike;
253
+ /** `new maplibregl.Marker(options)`. */
254
+ Marker: new (options: {
255
+ element: HTMLElement;
256
+ anchor?: "center" | "bottom";
257
+ offset?: readonly [number, number];
258
+ }) => MarkerLike;
259
+ /** `new maplibregl.Popup(options)`. */
260
+ Popup: new (options: Record<string, unknown>) => PopupLike;
261
+ }
262
+ /** A map instance. */
263
+ interface MapLike {
264
+ /** Subscribe to a map event ("load", "moveend", "render", "click", "zoomstart"). */
265
+ on(type: string, listener: (event?: unknown) => void): void;
266
+ /** Unsubscribe a listener `on` registered (the binders do on dispose: the caller owns the map and may
267
+ * keep it across rebinds). */
268
+ off(type: string, listener: (event?: unknown) => void): void;
269
+ /** Subscribe to the next occurrence of a map event. */
270
+ once(type: string, listener: () => void): void;
271
+ /** The visible area. */
272
+ getBounds(): {
273
+ getWest(): number;
274
+ getSouth(): number;
275
+ getEast(): number;
276
+ getNorth(): number;
277
+ };
278
+ /** The element the map renders into. */
279
+ getContainer(): HTMLElement;
280
+ /** Frame bounds (`[[w, s], [e, n]]`). */
281
+ fitBounds(bounds: unknown, options: Record<string, unknown>): void;
282
+ /** Animate the camera. */
283
+ easeTo(options: Record<string, unknown>): void;
284
+ /** One zoom step in. */
285
+ zoomIn(): void;
286
+ /** One zoom step out. */
287
+ zoomOut(): void;
288
+ /** Re-measure the container. */
289
+ resize(): void;
290
+ /** Destroy the map. */
291
+ remove(): void;
292
+ /** Add a source. */
293
+ addSource(id: string, source: Record<string, unknown>): void;
294
+ /** A source by id. */
295
+ getSource(id: string): GeoJsonSourceLike | undefined;
296
+ /** Add a style layer. */
297
+ addLayer(layer: Record<string, unknown>): void;
298
+ /** The source's features in the loaded tiles (a feature repeats across tiles). */
299
+ querySourceFeatures(id: string): ReadonlyArray<{
300
+ properties: Record<string, unknown>;
301
+ geometry: {
302
+ coordinates: unknown;
303
+ };
304
+ }>;
305
+ /** Whether the source's tiles are loaded. */
306
+ isSourceLoaded(id: string): boolean;
307
+ }
308
+ /** A GeoJSON source. */
309
+ interface GeoJsonSourceLike {
310
+ /** Replace the data. */
311
+ setData(data: unknown): void;
312
+ /** The zoom at which a cluster breaks apart. */
313
+ getClusterExpansionZoom(clusterId: number): Promise<number>;
314
+ /** The points inside a cluster. */
315
+ getClusterLeaves(clusterId: number, limit: number, offset: number): Promise<ReadonlyArray<{
316
+ properties: Record<string, unknown>;
317
+ }>>;
318
+ }
319
+ /** An HTML marker. */
320
+ interface MarkerLike {
321
+ /** Position it. */
322
+ setLngLat(at: readonly [number, number]): MarkerLike;
323
+ /** Pixel offset (folded into MapLibre's own transform). */
324
+ setOffset(offset: readonly [number, number]): MarkerLike;
325
+ /** Show it on a map. */
326
+ addTo(map: MapLike): MarkerLike;
327
+ /** Take it off the map. */
328
+ remove(): void;
329
+ }
330
+ /** A popup. */
331
+ interface PopupLike {
332
+ /** Position it. */
333
+ setLngLat(at: readonly [number, number]): PopupLike;
334
+ /** Its content. */
335
+ setDOMContent(node: Node): PopupLike;
336
+ /** Show it on a map. */
337
+ addTo(map: MapLike): PopupLike;
338
+ /** Close it. */
339
+ remove(): void;
340
+ }
341
+ /** Supplied by the caller: `() => import("maplibre-gl")`. Called at most once per page, however many maps
342
+ * build (the first loader handed in is the one used). */
343
+ type MapLibreLoader = () => Promise<MapLibreLike | {
344
+ default: MapLibreLike;
345
+ }>;
346
+ //#endregion
347
+ //#region src/enhance/map/place-markers.d.ts
348
+ /** The markers and how they look and speak. */
349
+ interface PlaceMarkersOptions {
350
+ /** One per spot (`mergePlaces` already joined places on one spot). */
351
+ markers: readonly PlaceMarker[];
352
+ /** "bottom" for a teardrop pin (its tip on the spot), "center" for a dot. */
353
+ anchor: "bottom" | "center";
354
+ /** Render each marker's body (`data-part="map-marker"`). */
355
+ render: (body: HTMLSpanElement, marker: PlaceMarker) => void;
356
+ /** Popup text per marker (the caller joins the names), shown on activation; null = a silent marker
357
+ * (a `div`, not a button). One popup at a time; a map click closes it. */
358
+ popup: ((marker: PlaceMarker) => string) | null;
359
+ }
360
+ /** Put the markers on the map; the Dispose takes them and the popup off. */
361
+ type BindPlaceMarkers = (map: MapLike, lib: MapLibreLike, options: PlaceMarkersOptions) => Dispose;
362
+ /** See {@link BindPlaceMarkers}. */
363
+ export declare const bindPlaceMarkers: BindPlaceMarkers;
364
+ //#endregion
365
+ //#region src/enhance/map/result-markers.d.ts
366
+ /** A marker on screen. The body carries `data-part` "map-pin" (with `data-id`), "map-cluster" or "map-group"
367
+ * (with `data-leaf-ids`, "|"-joined, for the list → map hover; a group also `data-expanded` while open). */
368
+ type ResultMarker = {
369
+ /** One rental. */
370
+ kind: "pin";
371
+ /** `p<id>`. */
372
+ key: string;
373
+ /** The rental id. */
374
+ id: string;
375
+ /** Render the pin here. */
376
+ body: HTMLSpanElement;
377
+ /** The element MapLibre positions (the button; put the accessible name here). */
378
+ shell: HTMLElement;
379
+ } | {
380
+ /** A cluster of the source (`c<cluster_id>`) or a same-spot group (`g<key>`). */
381
+ kind: "cluster" | "group";
382
+ /** `c<cluster_id>` / `g<key>`. */
383
+ key: string;
384
+ /** Rentals inside (a cluster sums its groups' sizes). */
385
+ count: number;
386
+ /** The rentals inside; a cluster's resolve asynchronously (empty until then, then onMarkers again). */
387
+ leafIds: readonly string[];
388
+ /** Render the bubble here. */
389
+ body: HTMLSpanElement;
390
+ /** The element MapLibre positions. */
391
+ shell: HTMLElement;
392
+ };
393
+ /** What the markers show and whom they tell. */
394
+ interface ResultMarkersOptions {
395
+ /** The pins (ResultsMapState.points). */
396
+ points: readonly MapPoint[];
397
+ /** Same-spot offsets and groups (ResultsMapState.layout). */
398
+ layout: SameSpotLayout;
399
+ /** false = no clustering. With `whenPriced`, a priced set clusters with those settings. */
400
+ cluster: false | ClusterOptions;
401
+ /** The pins carry prices (ResultsMapState.priced). */
402
+ priced: boolean;
403
+ /** The on-screen markers changed (a marker came or went, a cluster's leaves resolved); not called per frame. */
404
+ onMarkers: (markers: readonly ResultMarker[]) => void;
405
+ /** A pin was activated: a release within CLICK_TOLERANCE_PX of the press, or Enter/Space. */
406
+ onPin: (id: string) => void;
407
+ /** A group bubble was activated (the caller toggles it, then calls `expand`). */
408
+ onGroup: (key: string) => void;
409
+ /** A map click outside every marker, or a zoom start (a fanned group no longer sits on its spot). */
410
+ onBackground: () => void;
411
+ }
412
+ /** The bound markers. */
413
+ interface ResultMarkers {
414
+ /** New points or layout (favourites, campaign chips, a measured pin width). */
415
+ update(points: readonly MapPoint[], layout: SameSpotLayout, priced: boolean): void;
416
+ /** Fan one group out on its circle around its spot (and centre the map on it), or collapse (null). */
417
+ expand(groupKey: string | null, pinWidthPx: number): void;
418
+ /** Take every marker off the map and stop listening (the map itself stays the caller's). */
419
+ dispose(): void;
420
+ }
421
+ /** A clustered GeoJSON source (cluster counts sum group sizes through `clusterProperties`), an invisible
422
+ * circle layer so the source loads, and the HTML-marker diff on every `render`, keyed `c<cluster_id>` /
423
+ * `p<id>` / `g<key>` and deduped across tiles. A cluster activation eases to its expansion zoom. */
424
+ type BindResultMarkers = (map: MapLike, lib: MapLibreLike, options: ResultMarkersOptions) => ResultMarkers;
425
+ /** See {@link BindResultMarkers}. */
426
+ export declare const bindResultMarkers: BindResultMarkers;
427
+ //#endregion
428
+ //#region src/enhance/map/surface.d.ts
429
+ /** Moves shorter than this between press and release are a click, on the map (`clickTolerance`) AND on a
430
+ * pin: one number in two places, because two thresholds deciding the same thing disagreed (MapLibre's
431
+ * default 3 px started a drag that carried the pin away from under the pointer). Half a pin's width:
432
+ * the blur that aiming itself has. */
433
+ export declare const CLICK_TOLERANCE_PX: 12;
434
+ /** How a surface frames itself when it is built. */
435
+ type MapCamera = {
436
+ center: LngLat;
437
+ zoom: number;
438
+ } |
439
+ /** A thunk: a surface built later frames what exists by then. */
440
+ {
441
+ bounds: () => MapBounds;
442
+ padding: number;
443
+ maxZoom: number;
444
+ };
445
+ /** One surface. */
446
+ interface MapSurfaceOptions {
447
+ /** The element MapLibre renders into. */
448
+ container: HTMLElement;
449
+ /** `() => import("maplibre-gl")`. */
450
+ load: MapLibreLoader;
451
+ /** Vector-tile style URL. */
452
+ styleUrl: string;
453
+ /** The first frame. */
454
+ camera: MapCamera;
455
+ /** Wheel over the map zooms it instead of scrolling the page. */
456
+ scrollZoom: boolean;
457
+ /** "near": build once the container is within `nearMargin` of the viewport AND has a real width.
458
+ * "demand": build on `build()` only (layers, click-to-load). */
459
+ start: "near" | "demand";
460
+ /** IntersectionObserver rootMargin. Default "100% 0px" (one screen of lead). */
461
+ nearMargin?: string;
462
+ /** The map has loaded its style: put contents on it. Runs once per surface. */
463
+ onReady?: (map: MapLike, lib: MapLibreLike) => void;
464
+ /** After every moveend and every size change; a 0×0 container reports `bounds: null` (a hidden map must
465
+ * not filter the list). `byGuest`: pointer, wheel, touch or key input on the container, or a zoom
466
+ * button (`zoomIn`/`zoomOut`), since the previous report; a programmatic `fit()` never is. */
467
+ onView?: (bounds: MapBounds | null, byGuest: boolean) => void;
468
+ }
469
+ /** A surface. */
470
+ interface MapSurface {
471
+ /** Build now if not built (no-op when built or building); resolves once the map exists. */
472
+ build(): Promise<void>;
473
+ /** The map, or null while not built. */
474
+ map(): MapLike | null;
475
+ /** One zoom step in; counts as the guest's move. */
476
+ zoomIn(): void;
477
+ /** One zoom step out; counts as the guest's move. */
478
+ zoomOut(): void;
479
+ /** Programmatic, no animation; never counts as the guest's move. */
480
+ fit(bounds: MapBounds, padding: number, maxZoom: number): void;
481
+ /** Re-measure after a layer reopens (its container was resized while hidden). */
482
+ resize(): void;
483
+ /** Remove the map and every observer and listener. */
484
+ destroy(): void;
485
+ }
486
+ /** One MapLibre map on one container, with `attributionControl: { compact: false }` always (the compact
487
+ * credit collapses on the first touch and the OSM credit disappears) and `clickTolerance` 12. */
488
+ type CreateMapSurface = (options: MapSurfaceOptions) => MapSurface;
489
+ /** The marker element in two nodes: a shell MapLibre rewrites every frame (its inline transform positions
490
+ * the marker; anything CSS puts there is overwritten, and `scale` scales the translation with it) and a
491
+ * body for every visual (rotation, scale, the hover pop), which carries `data-part`. Never style the shell.
492
+ * A "button" shell is `type="button"`. */
493
+ type MarkerElement = (spec: {
494
+ tag: "button" | "div";
495
+ part: string;
496
+ }) => {
497
+ el: HTMLElement;
498
+ body: HTMLSpanElement;
499
+ };
500
+ /** See {@link CreateMapSurface}. */
501
+ export declare const createMapSurface: CreateMapSurface;
502
+ /** See {@link MarkerElement}. */
503
+ export declare const markerElement: MarkerElement;
183
504
  //#endregion
184
- export { type GalleryLayer, type GalleryOptions, type HistoryOverlayOptions, focusForKeyboard, initGallery, initHistoryOverlay, initImageCarousels, initRentalCarousel, trackInputModality, withViewTransition };
505
+ export { type Dispose, ENHANCE_PARTS, type EnhanceOptions, type EnhancePart, type EnhanceParts, type FavoritesBinding, GATE_MARKERS, type GateMarkers, type GeoJsonSourceLike, type LayerHandle, type LayerOptions, type MapCamera, type MapLibreLike, type MapLibreLoader, type MapLike, type MapSurface, type MapSurfaceOptions, type MarkerLike, type PlaceMarkersOptions, type PopupLike, type ResultMarker, type ResultMarkers, type ResultMarkersOptions, type RevealOptions, type ShareOptions, type StripHandle, type StripOptions };