diff --git a/packages/docs/components/MapboxMap/examples.md b/packages/docs/components/MapboxMap/examples.md
index 10660f8d..ebb6232f 100644
--- a/packages/docs/components/MapboxMap/examples.md
+++ b/packages/docs/components/MapboxMap/examples.md
@@ -4,7 +4,7 @@ title: MapboxMap examples
# Examples
-Every example below registers a single `MapboxMap` component — the child components (markers, popups, controls, sources, layers, images, clusters) are resolved automatically once the map is loaded. Each example loads the [Mapbox GL stylesheet](./#installation) from a CDN and picks a Mapbox style through the `map-options` option, which forwards any [`Map` option](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters) to mapbox-gl. Replace the access token with your own [access token](https://docs.mapbox.com/help/getting-started/access-tokens/); the token used here is a public, restricted demo token.
+Every component in the family is self-registering, so each example registers exactly the components it uses — `MapboxMap` on its own for a bare map, plus the child components it declares (markers, popups, controls, sources, layers, images, clusters). Each child resolves its parent `MapboxMap` on its own once mounted. Each example loads the [Mapbox GL stylesheet](./#installation) from a CDN and picks a Mapbox style through the `map-options` option, which forwards any [`Map` option](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters) to mapbox-gl. Replace the access token with your own [access token](https://docs.mapbox.com/help/getting-started/access-tokens/); the token used here is a public, restricted demo token.
Child components that should not take part in the normal document flow (markers, popups, controls, sources, layers, images, clusters) are wrapped in a `hidden` element: their markup is only used as a declarative definition, the actual rendering happens on the map canvas.
@@ -102,7 +102,7 @@ Register images against the map sprite so they can be referenced from a symbol l
## Cluster
-The `MapboxCluster` component displays a clustered GeoJSON source, wiring the cluster circles, the cluster count labels and the unclustered points, together with the click-to-zoom interaction. Its `data` option takes the URL of a `.geojson` file, or you can pass inline GeoJSON through a [`geojson` script ref](#inline-geojson-cluster). The example below passes the points inline; click a cluster to zoom in and split it.
+The `MapboxCluster` component is a clustered GeoJSON **source driver**: it derives its source from the `MapboxClusterItem`s in its subtree, wiring the cluster circles, the cluster count labels and the unclustered points, together with the click-to-zoom interaction. Each item is at once a list entry and a map feature — the same markup drives both. The example below declares a hidden list of items; click a cluster to zoom in and split it.
-### Inline GeoJSON cluster {#inline-geojson-cluster}
+### Items drive the source {#cluster-items}
-Instead of a `data` URL, pass the GeoJSON inline through a `
+
+
+
+
+
+
```
+The cluster reports a click on an unclustered point through its `item-click` event but never selects or flies on its own. To turn this into a full "find a store near you" experience — selection, popups, viewport filtering and address search — wrap the cluster in a [`StoreLocator`](/components/StoreLocator/) orchestrator.
+
## Listening to map events
The `MapboxMap` component re-emits the Mapbox map events. Listen to them from a parent component by defining `on` methods — for example `onMapboxMapClick` or `onMapboxMapMapLoad` for the custom `map-load` event.
diff --git a/packages/docs/components/MapboxMap/index.md b/packages/docs/components/MapboxMap/index.md
index 96ac9328..4bf25245 100644
--- a/packages/docs/components/MapboxMap/index.md
+++ b/packages/docs/components/MapboxMap/index.md
@@ -4,7 +4,7 @@ badges: [JS]
# MapboxMap
-The `MapboxMap` component and its family let you build [Mapbox GL](https://docs.mapbox.com/mapbox-gl-js/) maps declaratively, straight from your HTML, with [js-toolkit](https://js-toolkit.studiometa.dev/) — no Vue, no framework runtime. A `MapboxMap` element owns the underlying Mapbox `Map` instance, and every other component (markers, popups, controls, sources, layers, images, clusters) is authored as a child element that registers itself against that map once it is loaded.
+The `MapboxMap` component and its family let you build [Mapbox GL](https://docs.mapbox.com/mapbox-gl-js/) maps declaratively, straight from your HTML, with [js-toolkit](https://js-toolkit.studiometa.dev/). A `MapboxMap` element owns the underlying Mapbox `Map` instance, and every other component (markers, popups, controls, sources, layers, images, clusters) is authored as a child element that registers itself against that map once it is loaded.
These components are published in the standalone [`@studiometa/ui-mapbox`](https://www.npmjs.com/package/@studiometa/ui-mapbox) package and replace the [`@studiometa/vue-mapbox-gl`](https://www.npmjs.com/package/@studiometa/vue-mapbox-gl) library. If you are coming from the Vue library, read the [migration guide](/migration-guides/vue-mapbox-gl/).
@@ -39,7 +39,7 @@ You will also need a [Mapbox access token](https://docs.mapbox.com/help/getting-
## Usage
-Register the `MapboxMap` component with [`registerComponent`](https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html). All child components are declared internally, so you only ever register `MapboxMap` itself — they are resolved automatically once the map is loaded.
+[Register](/guide/usage/#registering-components) each component your page uses: a bare map with just a `container` needs only `MapboxMap`, and each marker, popup, control, source, layer, image or cluster you declare must be registered too. Registration order does not matter, because a child registered before its `MapboxMap` still wires up once the map connects.
Author the map with a root `MapboxMap` element holding a `container` ref, and give it a size through CSS.
@@ -69,22 +69,22 @@ registerComponent(MapboxMap);
:::
-::: tip Options are read once at mount
-Component options are read a single time when the map mounts and are **not** reactive, unlike the Vue library. To move the map or update its data afterwards, call the underlying Mapbox objects directly (e.g. `instance.map.setCenter(...)`). See the [reactivity note](./js-api#reactivity-and-updates) for details.
-:::
+`mapbox-gl` is a heavy dependency (~230 kB gzipped, more with the geocoder), so the recommended default is to register each component lazily — see [Lazy loading](#lazy-loading) below.
## Lazy loading
-`mapbox-gl` is a heavy dependency (~230 kB gzipped, more with the geocoder). Registering `MapboxMap` eagerly pulls it into your main bundle even on pages that never render a map. Prefer **lazy registration** so the map is code-split into its own chunk and only loaded when it is actually needed.
+Keep `mapbox-gl` out of your main bundle by lazy-registering each component with js-toolkit's [`importWhen*` helpers](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) and the per-component subpaths. `importWhenVisible` defers the dynamic import until a `MapboxMap` element scrolls into view, then hands the resolved component to `registerComponent`, code-splitting `mapbox-gl` into its own chunk loaded only when a map is actually needed.
-js-toolkit ships [`importWhen*` helpers](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) for this. `importWhenVisible` defers the dynamic import until a `MapboxMap` element scrolls into view, then hands the resolved component to `registerComponent`:
+Every component is available at its own subpath (`@studiometa/ui-mapbox/`), whose default export is the component class — so the dynamic import needs no destructuring. Because each component is registered independently, lazy-register each one you use: deferring `MapboxMap` pulls in `mapbox-gl`, but a marker or a cluster is its own module and must get its own lazy registration.
```js
-import { registerComponent, importWhenVisible } from '@studiometa/js-toolkit';
+import { registerComponents, importWhenVisible } from '@studiometa/js-toolkit';
-registerComponent(importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMap'), 'MapboxMap'));
+registerComponents(
+ importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMap'), 'MapboxMap'),
+ importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMarker'), 'MapboxMarker'),
+ importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxPopup'), 'MapboxPopup'),
+);
```
-Every component is also available at its own subpath (`@studiometa/ui-mapbox/`), whose default export is the component class — so the dynamic import needs no destructuring.
-
-Because every marker, popup, control, source, layer, image and cluster is a child of `MapboxMap`, deferring the map defers the whole family — `mapbox-gl` included. Reach for a different trigger when it fits better: `importWhenIdle` (load during browser idle time), `importOnInteraction` (wait for a first click/focus/touch on the element) or `importOnMediaQuery` (load only above a breakpoint, e.g. to skip the map on small screens).
+Reach for a different trigger when it fits better: `importWhenIdle` (load during browser idle time), `importOnInteraction` (wait for a first click/focus/touch on the element) or `importOnMediaQuery` (load only above a breakpoint, e.g. to skip the map on small screens).
diff --git a/packages/docs/components/MapboxMap/js-api.md b/packages/docs/components/MapboxMap/js-api.md
index f1c66eaf..b9137450 100644
--- a/packages/docs/components/MapboxMap/js-api.md
+++ b/packages/docs/components/MapboxMap/js-api.md
@@ -5,21 +5,19 @@ outline: deep
# JS API
-The `@studiometa/ui-mapbox` package exposes twelve components plus the `AbstractMapboxMapChild` base class. They are organized around a single root component, `MapboxMap`, which owns the Mapbox `Map` instance. Every other component is a child that resolves the closest parent `MapboxMap` and registers itself against its map once it is loaded.
+This page documents thirteen components — a single root component, `MapboxMap`, which owns the Mapbox `Map` instance, plus the twelve children below — built on the `AbstractMapboxMapChild` (and, for controls, `AbstractMapboxControl`) base classes. The [`StoreLocator`](/components/StoreLocator/) orchestrator completes the family on its own page. Every child resolves the closest parent `MapboxMap` on its own and registers itself against its map once it is loaded.
-You only ever register `MapboxMap` with [`registerComponent`](https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html): all child components are declared internally and resolved automatically.
+[Register](/guide/usage/#registering-components) each component you use — ideally behind a lazy [`importWhen*` helper](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) so the heavy `mapbox-gl` dependency stays out of your main bundle (see [Lazy loading](/components/MapboxMap/#lazy-loading)).
- **[Map](#map)** — `MapboxMap`
- **[Markers & Popups](#markers-popups)** — `MapboxMarker`, `MapboxPopup`
- **[Controls](#controls)** — `MapboxNavigationControl`, `MapboxGeolocateControl`, `MapboxFullscreenControl`, `MapboxGeocoder`
- **[Data](#data)** — `MapboxSource`, `MapboxLayer`, `MapboxImage`, `MapboxImages`
-- **[Cluster](#cluster)** — `MapboxCluster`
+- **[Cluster](#cluster)** — `MapboxCluster`, `MapboxClusterItem` (see also the [`StoreLocator`](/components/StoreLocator/) orchestrator)
- **[AbstractMapboxMapChild](#abstractmapboxmapchild)** — the shared base class
## Reactivity and updates
-Component options are read **once, at mount time**, and are **not reactive**. This is a deliberate behavioral difference from the `@studiometa/vue-mapbox-gl` library, where changing a prop updated the map.
-
To move the map, change its data or update a marker after mount, reach for the underlying Mapbox objects directly through the component instances:
```js
@@ -36,6 +34,10 @@ marker.marker.setLngLat([2.29, 48.86]);
Every component documents the Mapbox object it exposes (`map`, `marker`, `popup`, `control`, …) in its **Getters** section below.
+::: tip Switching base style keeps your layers
+Calling `map.setStyle(…)` wipes the entire style — every source, layer and sprite — while the map instance and the still-mounted child components survive. Each map child subscribes to the map's `style.load` and **re-injects** its contribution automatically, so your declarative sources, layers, images and clusters re-appear on the new base style instead of silently vanishing. You can switch the base style at runtime without re-declaring your data.
+:::
+
## Map
### MapboxMap
@@ -44,12 +46,31 @@ Display an interactive Mapbox GL map. This is the root component of the system a
#### Options
-| Option | Type | Default | Description |
-| -------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `access-token` | `String` | — | Mapbox GL [access token](https://docs.mapbox.com/help/getting-started/access-tokens/) (required). |
-| `zoom` | `Number` | — | Initial zoom level. |
-| `center` | `Array` | `[0, 0]` | Initial center as `[longitude, latitude]`. |
-| `map-options` | `Object` | `{}` | Any other [Mapbox `Map` option](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters), spread into the constructor. This is where `style`, `pitch`, `bearing`, `bounds`, … go. |
+##### `access-token`
+
+- Type: `String`
+
+Mapbox GL [access token](https://docs.mapbox.com/help/getting-started/access-tokens/) (required).
+
+##### `zoom`
+
+- Type: `Number`
+
+Initial zoom level.
+
+##### `center`
+
+- Type: `Array`
+- Default: `[0, 0]`
+
+Initial center as `[longitude, latitude]`.
+
+##### `map-options`
+
+- Type: `Object`
+- Default: `{}`
+
+Any other [Mapbox `Map` option](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters), spread into the constructor. This is where `style`, `pitch`, `bearing`, `bounds`, … go.
The `access-token`, `zoom` and `center` options act as overridable defaults, then `map-options` is spread into the [Mapbox `Map` constructor](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters). The `container` is always resolved from the component (the `container` ref, falling back to the root element) and can not be overridden. Use `map-options` for anything the convenience options above do not cover — most notably the map [`style`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map-parameters):
@@ -64,50 +85,137 @@ The `access-token`, `zoom` and `center` options act as overridable defaults, the
#### Refs
-| Ref | Type | Description |
-| ----------- | ------------- | ----------------------------------------------------------------------------------- |
-| `container` | `HTMLElement` | The element used as the map container. Falls back to the root element when omitted. |
+##### `container`
+
+- Type: `HTMLElement`
+
+The element used as the map container. Falls back to the root element when omitted.
#### Getters
-| Getter | Type | Description |
-| ---------- | -------------- | -------------------------------------- |
-| `map` | `mapboxgl.Map` | The underlying Mapbox GL map instance. |
-| `isLoaded` | `boolean` | Whether the map has finished loading. |
+##### `map`
+
+- Type: `mapboxgl.Map`
+
+The underlying Mapbox GL map instance.
+
+##### `isLoaded`
+
+- Type: `boolean`
+
+Whether the map has finished loading.
#### Events
The component emits a custom `map-load` event, plus all the common Mapbox GL map events, re-emitted under the same name. Each handler receives the corresponding Mapbox event object (the `map-load` handler receives the `map` instance).
-| Event | Description |
-| ------------- | ---------------------------------------- |
-| `map-load` | The map finished loading (custom event). |
-| `load` | Map resources are loaded. |
-| `idle` | The map is idle after rendering. |
-| `render` | A frame is rendered. |
-| `resize` | The map container is resized. |
-| `remove` | The map is removed. |
-| `error` | An error occurred. |
-| `click` | A click on the map. |
-| `dblclick` | A double-click on the map. |
-| `mouseenter` | The pointer enters the map canvas. |
-| `mouseleave` | The pointer leaves the map canvas. |
-| `mousemove` | The pointer moves over the map. |
-| `movestart` | Map movement starts (pan, zoom, rotate). |
-| `move` | The map is moving. |
-| `moveend` | Map movement ends. |
-| `zoomstart` | A zoom transition starts. |
-| `zoom` | The zoom level changes. |
-| `zoomend` | A zoom transition ends. |
-| `rotatestart` | Rotation starts. |
-| `rotate` | The map is rotating. |
-| `rotateend` | Rotation ends. |
-| `pitchstart` | A pitch transition starts. |
-| `pitch` | The pitch changes. |
-| `pitchend` | A pitch transition ends. |
-| `dragstart` | A drag starts. |
-| `drag` | The map is being dragged. |
-| `dragend` | A drag ends. |
+##### `map-load`
+
+The map finished loading (custom event).
+
+##### `load`
+
+Map resources are loaded.
+
+##### `idle`
+
+The map is idle after rendering.
+
+##### `render`
+
+A frame is rendered.
+
+##### `resize`
+
+The map container is resized.
+
+##### `remove`
+
+The map is removed.
+
+##### `error`
+
+An error occurred.
+
+##### `click`
+
+A click on the map.
+
+##### `dblclick`
+
+A double-click on the map.
+
+##### `mouseenter`
+
+The pointer enters the map canvas.
+
+##### `mouseleave`
+
+The pointer leaves the map canvas.
+
+##### `mousemove`
+
+The pointer moves over the map.
+
+##### `movestart`
+
+Map movement starts (pan, zoom, rotate).
+
+##### `move`
+
+The map is moving.
+
+##### `moveend`
+
+Map movement ends.
+
+##### `zoomstart`
+
+A zoom transition starts.
+
+##### `zoom`
+
+The zoom level changes.
+
+##### `zoomend`
+
+A zoom transition ends.
+
+##### `rotatestart`
+
+Rotation starts.
+
+##### `rotate`
+
+The map is rotating.
+
+##### `rotateend`
+
+Rotation ends.
+
+##### `pitchstart`
+
+A pitch transition starts.
+
+##### `pitch`
+
+The pitch changes.
+
+##### `pitchend`
+
+A pitch transition ends.
+
+##### `dragstart`
+
+A drag starts.
+
+##### `drag`
+
+The map is being dragged.
+
+##### `dragend`
+
+A drag ends.
Listen to these events from a parent component with `on` methods:
@@ -145,18 +253,33 @@ Add a [marker](https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker) to the
#### Options
-| Option | Type | Default | Description |
-| ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
-| `lng-lat` | `Array` | `[0, 0]` | Marker position as `[longitude, latitude]`. |
-| `marker-options` | `Object` | `{}` | [Mapbox Marker options](https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker) (color, anchor, draggable, …). |
+##### `lng-lat`
+
+- Type: `Array`
+- Default: `[0, 0]`
+
+Marker position as `[longitude, latitude]`.
+
+##### `marker-options`
+
+- Type: `Object`
+- Default: `{}`
+
+[Mapbox Marker options](https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker) (color, anchor, draggable, …).
#### Getters
-| Getter | Type | Description |
-| --------------- | ----------------- | --------------------------------------------- |
-| `marker` | `mapboxgl.Marker` | The underlying Marker instance. |
-| `popup` | `MapboxPopup` | The first nested `MapboxPopup` child, if any. |
-| `markerOptions` | `Object` | The resolved marker options. |
+##### `marker`
+
+- Type: `mapboxgl.Marker`
+
+The underlying Marker instance.
+
+##### `popup`
+
+- Type: `MapboxPopup`
+
+The first nested `MapboxPopup` child, if any.
### MapboxPopup
@@ -164,17 +287,27 @@ Display a [popup](https://docs.mapbox.com/mapbox-gl-js/api/markers/#popup) on th
#### Options
-| Option | Type | Default | Description |
-| --------------- | -------- | -------- | -------------------------------------------------------------------------------- |
-| `lng-lat` | `Array` | `[0, 0]` | Popup position as `[longitude, latitude]` (standalone popups only). |
-| `popup-options` | `Object` | `{}` | [Mapbox Popup options](https://docs.mapbox.com/mapbox-gl-js/api/markers/#popup). |
+##### `lng-lat`
+
+- Type: `Array`
+- Default: `[0, 0]`
+
+Popup position as `[longitude, latitude]` (standalone popups only).
+
+##### `popup-options`
+
+- Type: `Object`
+- Default: `{}`
+
+[Mapbox Popup options](https://docs.mapbox.com/mapbox-gl-js/api/markers/#popup).
#### Getters
-| Getter | Type | Description |
-| -------------- | ---------------- | ------------------------------ |
-| `popup` | `mapboxgl.Popup` | The underlying Popup instance. |
-| `popupOptions` | `Object` | The resolved popup options. |
+##### `popup`
+
+- Type: `mapboxgl.Popup`
+
+The underlying Popup instance.
## Controls
@@ -186,12 +319,33 @@ Add zoom in/out and compass controls to the map.
#### Options
-| Option | Type | Default | Description |
-| ----------------- | --------- | ------------- | --------------------------------------------------------------------------- |
-| `position` | `String` | `'top-right'` | Control position: `top-left`, `top-right`, `bottom-left` or `bottom-right`. |
-| `show-compass` | `Boolean` | `false` | Show the compass button. |
-| `show-zoom` | `Boolean` | `false` | Show the zoom in/out buttons. |
-| `visualize-pitch` | `Boolean` | `false` | Visualize the pitch on the compass button. |
+##### `position`
+
+- Type: `String`
+- Default: `'top-right'`
+
+Control position: `top-left`, `top-right`, `bottom-left` or `bottom-right`.
+
+##### `show-compass`
+
+- Type: `Boolean`
+- Default: `false`
+
+Show the compass button.
+
+##### `show-zoom`
+
+- Type: `Boolean`
+- Default: `false`
+
+Show the zoom in/out buttons.
+
+##### `visualize-pitch`
+
+- Type: `Boolean`
+- Default: `false`
+
+Visualize the pitch on the compass button.
### MapboxGeolocateControl
@@ -199,15 +353,52 @@ Add a button that uses the browser's geolocation API to locate the user on the m
#### Options
-| Option | Type | Default | Description |
-| ---------------------- | --------- | ------------- | ----------------------------------------------------------------------------------------------------- |
-| `position` | `String` | `'top-right'` | Control position. |
-| `position-options` | `Object` | — | Browser [`PositionOptions`](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions). |
-| `fit-bounds-options` | `Object` | — | [`FitBoundsOptions`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#fitbounds) used when tracking. |
-| `track-user-location` | `Boolean` | `false` | Continuously track the user location. |
-| `show-accuracy-circle` | `Boolean` | `false` | Show the accuracy circle around the user location. |
-| `show-user-location` | `Boolean` | `false` | Show the user location dot. |
-| `show-user-heading` | `Boolean` | `false` | Show the user heading indicator. |
+##### `position`
+
+- Type: `String`
+- Default: `'top-right'`
+
+Control position.
+
+##### `position-options`
+
+- Type: `Object`
+
+Browser [`PositionOptions`](https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions).
+
+##### `fit-bounds-options`
+
+- Type: `Object`
+
+[`FitBoundsOptions`](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#fitbounds) used when tracking.
+
+##### `track-user-location`
+
+- Type: `Boolean`
+- Default: `false`
+
+Continuously track the user location.
+
+##### `show-accuracy-circle`
+
+- Type: `Boolean`
+- Default: `false`
+
+Show the accuracy circle around the user location.
+
+##### `show-user-location`
+
+- Type: `Boolean`
+- Default: `false`
+
+Show the user location dot.
+
+##### `show-user-heading`
+
+- Type: `Boolean`
+- Default: `false`
+
+Show the user heading indicator.
### MapboxFullscreenControl
@@ -215,9 +406,12 @@ Add a button that toggles the map fullscreen.
#### Options
-| Option | Type | Default | Description |
-| ---------- | -------- | ------------- | ----------------- |
-| `position` | `String` | `'top-right'` | Control position. |
+##### `position`
+
+- Type: `String`
+- Default: `'top-right'`
+
+Control position.
### MapboxGeocoder
@@ -229,19 +423,43 @@ Add an address search control powered by [`@mapbox/mapbox-gl-geocoder`](https://
#### Options
-| Option | Type | Default | Description |
-| ------------ | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `add-to-map` | `Boolean` | `false` | When `true`, the geocoder is added to the map as a control. Otherwise it is rendered inside the component's element. |
-| `options` | `Object` | `{}` | [Geocoder options](https://github.com/mapbox/mapbox-gl-geocoder/blob/master/API.md#parameters). Non-serializable options (`filter`, `externalGeocoder`, `render`, `getItemValue`, `localGeocoder`) are not supported. |
+##### `add-to-map`
+
+- Type: `Boolean`
+- Default: `false`
+
+Add the geocoder to the map as a control. Otherwise it is rendered inside the component's element.
+
+##### `options`
+
+- Type: `Object`
+- Default: `{}`
+
+[Geocoder options](https://github.com/mapbox/mapbox-gl-geocoder/blob/master/API.md#parameters). Non-serializable options (`filter`, `externalGeocoder`, `render`, `getItemValue`, `localGeocoder`) are not supported.
The `accessToken` is inherited from the parent `MapboxMap` when it is not set in `options`.
#### Getters
-| Getter | Type | Description |
-| --------- | -------------------- | -------------------------------------------------------- |
-| `control` | `MapboxGeocoder` | The underlying `mapbox-gl-geocoder` control instance. |
-| `target` | `Map \| HTMLElement` | Where the control is mounted, depending on `add-to-map`. |
+##### `control`
+
+- Type: `MapboxGeocoder`
+
+The underlying `mapbox-gl-geocoder` control instance.
+
+##### `target`
+
+- Type: `Map | HTMLElement`
+
+Where the control is mounted, depending on `add-to-map`.
+
+#### Events
+
+##### `result`
+
+- Payload: `result`
+
+Emitted when the geocoder resolves an address, carrying the geocoder's `result` (its selected feature).
## Data
@@ -251,16 +469,33 @@ Add a [source](https://docs.mapbox.com/style-spec/reference/sources/) to the map
#### Options
-| Option | Type | Default | Description |
-| -------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
-| `id` | `String` | — | Unique source id, referenced by layers. |
-| `source` | `Object` | — | A [source specification](https://docs.mapbox.com/style-spec/reference/sources/), e.g. `{ "type": "geojson", "data": … }`. |
+##### `id`
+
+- Type: `String`
+
+Unique source id, referenced by layers.
+
+##### `source`
+
+- Type: `Object`
+
+A [source specification](https://docs.mapbox.com/style-spec/reference/sources/), e.g. `{ "type": "geojson", "data": … }`.
#### Refs
-| Ref | Type | Description |
-| --------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `geojson` | `HTMLScriptElement` | Optional `
+
+ {% for point in points %}
+
+ {% endfor %}
+
diff --git a/packages/docs/components/MapboxMap/stories/controls/app.js b/packages/docs/components/MapboxMap/stories/controls/app.js
index e40fade7..51bd9354 100644
--- a/packages/docs/components/MapboxMap/stories/controls/app.js
+++ b/packages/docs/components/MapboxMap/stories/controls/app.js
@@ -1,4 +1,15 @@
-import { registerComponent } from '@studiometa/js-toolkit';
-import { MapboxMap } from '@studiometa/ui-mapbox';
+import { registerComponents } from '@studiometa/js-toolkit';
+import {
+ MapboxMap,
+ MapboxNavigationControl,
+ MapboxGeolocateControl,
+ MapboxFullscreenControl,
+} from '@studiometa/ui-mapbox';
-registerComponent(MapboxMap);
+// Register every component the markup uses.
+registerComponents(
+ MapboxMap,
+ MapboxNavigationControl,
+ MapboxGeolocateControl,
+ MapboxFullscreenControl,
+);
diff --git a/packages/docs/components/MapboxMap/stories/images/app.js b/packages/docs/components/MapboxMap/stories/images/app.js
index e40fade7..e1947f06 100644
--- a/packages/docs/components/MapboxMap/stories/images/app.js
+++ b/packages/docs/components/MapboxMap/stories/images/app.js
@@ -1,4 +1,5 @@
-import { registerComponent } from '@studiometa/js-toolkit';
-import { MapboxMap } from '@studiometa/ui-mapbox';
+import { registerComponents } from '@studiometa/js-toolkit';
+import { MapboxMap, MapboxImages, MapboxSource, MapboxLayer } from '@studiometa/ui-mapbox';
-registerComponent(MapboxMap);
+// Register every component the markup uses.
+registerComponents(MapboxMap, MapboxImages, MapboxSource, MapboxLayer);
diff --git a/packages/docs/components/MapboxMap/stories/markers/app.js b/packages/docs/components/MapboxMap/stories/markers/app.js
index e40fade7..58d2e4a4 100644
--- a/packages/docs/components/MapboxMap/stories/markers/app.js
+++ b/packages/docs/components/MapboxMap/stories/markers/app.js
@@ -1,4 +1,5 @@
-import { registerComponent } from '@studiometa/js-toolkit';
-import { MapboxMap } from '@studiometa/ui-mapbox';
+import { registerComponents } from '@studiometa/js-toolkit';
+import { MapboxMap, MapboxMarker, MapboxPopup } from '@studiometa/ui-mapbox';
-registerComponent(MapboxMap);
+// Register every component the markup uses.
+registerComponents(MapboxMap, MapboxMarker, MapboxPopup);
diff --git a/packages/docs/components/MapboxMap/stories/source-layer/app.js b/packages/docs/components/MapboxMap/stories/source-layer/app.js
index e40fade7..4c0e2ae8 100644
--- a/packages/docs/components/MapboxMap/stories/source-layer/app.js
+++ b/packages/docs/components/MapboxMap/stories/source-layer/app.js
@@ -1,4 +1,5 @@
-import { registerComponent } from '@studiometa/js-toolkit';
-import { MapboxMap } from '@studiometa/ui-mapbox';
+import { registerComponents } from '@studiometa/js-toolkit';
+import { MapboxMap, MapboxSource, MapboxLayer } from '@studiometa/ui-mapbox';
-registerComponent(MapboxMap);
+// Register every component the markup uses.
+registerComponents(MapboxMap, MapboxSource, MapboxLayer);
diff --git a/packages/docs/components/StoreLocator/examples.md b/packages/docs/components/StoreLocator/examples.md
index 342bedb0..87b5546c 100644
--- a/packages/docs/components/StoreLocator/examples.md
+++ b/packages/docs/components/StoreLocator/examples.md
@@ -4,19 +4,19 @@ title: StoreLocator examples
# Examples
-Both examples register a single root component that declares the `StoreLocator` — its `MapboxMap` and `StoreLocatorItem` children are resolved automatically, and the `MapboxCluster` inside the map is fed by the coordinator once the map has loaded. Each example loads the [Mapbox GL stylesheet](/components/MapboxMap/#installation) from a CDN and picks a Mapbox style through the `map-options` option. Replace the access token with your own [access token](https://docs.mapbox.com/help/getting-started/access-tokens/); the token used here is a public, restricted demo token.
+Both examples register the Mapbox components they use (`StoreLocator`, `MapboxMap`, `MapboxCluster`, `MapboxClusterItem`) independently — see [Usage](./#usage) for the recommended lazy registration — and add a small root component for the detail panel. The `StoreLocator` orchestrates a `MapboxMap` containing a `MapboxCluster` whose `MapboxClusterItem`s are the sidebar entries. Each example loads the [Mapbox GL stylesheet](/components/MapboxMap/#installation) from a CDN and picks a Mapbox style through the `map-options` option. Replace the access token with your own [access token](https://docs.mapbox.com/help/getting-started/access-tokens/); the token used here is a public, restricted demo token.
## Basic store locator
-A `StoreLocator` wrapping a sidebar list of Paris stores and a clustered map. Note that the `MapboxCluster` has **no** authored data: the coordinator derives a GeoJSON `FeatureCollection` from the list items and pushes it to the cluster after the map loads.
+A `StoreLocator` wrapping a clustered map whose `MapboxCluster` holds a sidebar list of Paris `MapboxClusterItem`s. The cluster has **no** authored data: it derives a GeoJSON `FeatureCollection` from its registered items, and the orchestrator adds selection, viewport filtering and the detail drawer on top.
Try it out:
- **Pan or zoom the map** — the sidebar filters to the in-view stores and reorders them nearest-first.
-- **Click a store in the list** — the map flies there, the item is marked active and the detail drawer opens.
+- **Click a store in the list** — the map flies there, the item is marked active, a popup opens and the detail drawer opens.
- **Click a cluster** — the map zooms in and splits it; click an individual pin to select its store.
-The detail panel is the integrator's choice. Here it is a [`Dialog`](/components/Dialog/) drawer, opened from the [`select`](./js-api#select) event through a small root component's `onStoreLocatorSelect` handler, which copies the selected item's `` detail into the drawer. A [`MapboxPopup`](/components/MapboxMap/js-api#mapboxpopup) or a static `aside` bound to the same event would work too.
+The detail panel is the integrator's choice. Here it is a [`Dialog`](/components/Dialog/) drawer, opened from the [`select`](./js-api#select) event through a small root component's `onStoreLocatorSelect` handler, which copies the selected item's `` detail into the drawer. A static `aside` bound to the same event would work too.
+
-
-
-
-
+
+
+
+
+
+
+
+
```
diff --git a/packages/docs/components/StoreLocator/index.md b/packages/docs/components/StoreLocator/index.md
index 183a618b..ff0160dd 100644
--- a/packages/docs/components/StoreLocator/index.md
+++ b/packages/docs/components/StoreLocator/index.md
@@ -4,25 +4,32 @@ badges: [JS]
# StoreLocator
-The `StoreLocator` component coordinates a "find a store near you" experience around a [`MapboxMap`](/components/MapboxMap/). It is a thin, composable coordinator: it owns no map rendering of its own and instead wires together a `MapboxMap`, an optional [`MapboxCluster`](/components/MapboxMap/js-api#cluster) (the map data source), an optional [`MapboxGeocoder`](/components/MapboxMap/js-api#mapboxgeocoder) (address search) and a sidebar list of `StoreLocatorItem`s — all authored declaratively from your HTML with [js-toolkit](https://js-toolkit.studiometa.dev/), no framework runtime.
+The `StoreLocator` component adds a "find a store near you" experience on top of a [`MapboxMap`](/components/MapboxMap/). It is a thin **orchestrator**: it owns no map rendering and no item registry of its own. It wraps a `MapboxMap` containing a [`MapboxCluster`](/components/MapboxMap/js-api#cluster) — the declarative clustered map **and** list source driver, whose [`MapboxClusterItem`](/components/MapboxMap/js-api#mapboxclusteritem)s are the sidebar entries — plus an optional [`MapboxGeocoder`](/components/MapboxMap/js-api#mapboxgeocoder) (address search), and layers the search UX on top: selection, viewport filtering and address search. Everything is authored declaratively from your HTML with [js-toolkit](https://js-toolkit.studiometa.dev/).
It is part of the [`@studiometa/ui-mapbox`](https://www.npmjs.com/package/@studiometa/ui-mapbox) package, alongside the rest of the [`MapboxMap` family](/components/MapboxMap/).
+## Cluster vs. orchestrator
+
+The responsibilities are split in two:
+
+- The **`MapboxCluster`** is a pure source driver. Its `MapboxClusterItem`s self-register, and it derives a clustered GeoJSON source from that registry (the map data), handles the click-to-zoom on clusters, and reports a click on an unclustered point through an `item-click` event. Used on its own it renders a working clustered map + list, but it never selects, flies, filters by viewport or opens popups.
+- The **`StoreLocator`** wraps such a cluster and adds exactly those search-UX concerns: it selects items (fly-to, `active` styling, popup), filters and sorts the list on every map move, wires the geocoder, and re-frames the map on item-set changes.
+
## The three-state model
-Each store has three independent states, each with its own source of truth. Keeping them separate is what makes the coordinator predictable — panning the map never rebuilds the map data, and updating the item set never fights with the current viewport.
+Each store has three independent states, each with its own source of truth. Keeping them separate is what makes the orchestrator predictable — panning the map never rebuilds the map data, and updating the item set never fights with the current viewport.
-| State | Source of truth | Drives | Recomputed on |
-| -------------- | ------------------------------------------ | ----------------------------------------------- | -------------------- |
-| **Registered** | the item exists in the DOM | the **map data** (markers/clusters) | item-set change only |
-| **In bounds** | the item's `lngLat` is inside the viewport | **list visibility + distance sort** only | every map `moveend` |
-| **Selected** | the chosen item | fly-to, `active` styling and the `select` event | selection |
+| State | Source of truth | Owner | Drives | Recomputed on |
+| -------------- | ------------------------------------------ | --------------- | ----------------------------------------------- | -------------------- |
+| **Registered** | the item exists in the DOM | `MapboxCluster` | the **map data** (markers/clusters) | item-set change only |
+| **In bounds** | the item's `lngLat` is inside the viewport | `StoreLocator` | **list visibility + distance sort** only | every map `moveend` |
+| **Selected** | the chosen item | `StoreLocator` | fly-to, popup, `active` styling, `select` event | selection |
-Because the map data is derived only from the registered items, swapping the list (for instance through a [`Fetch`](/components/Fetch/)) updates both the list **and** the map at once. See the [faceted example](./examples#faceted-list).
+Because the map data is derived only from the cluster's registered items, swapping the list (for instance through a [`Fetch`](/components/Fetch/)) updates both the list **and** the map at once: the cluster re-derives its source and emits an `update`, and the orchestrator re-fits and re-filters in response. See the [faceted example](./examples#faceted-list).
## Composability
-The `StoreLocator` emits events and reflects state as data-attributes; it makes no decision about how a selected store is presented. The [examples](./examples.md) use a [`Dialog`](/components/Dialog/) drawer as the detail panel, but a [`MapboxPopup`](/components/MapboxMap/js-api#mapboxpopup) or a plain static `aside` would work just as well — wire whichever you like to the [`select`](./js-api#select) event.
+The `StoreLocator` emits events and reflects state as data-attributes on each `MapboxClusterItem`; it makes no decision about how a selected store is presented beyond the built-in popup. The [examples](./examples.md) use a [`Dialog`](/components/Dialog/) drawer as the detail panel, but a static `aside` would work just as well — wire whichever you like to the [`select`](./js-api#select) event.
## Table of content
@@ -41,41 +48,47 @@ The [Mapbox GL stylesheet](/components/MapboxMap/#installation) and a [Mapbox ac
## Usage
-Register only the `StoreLocator` with [`registerComponent`](https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html): it declares `MapboxMap` and `StoreLocatorItem` internally, and the `MapboxCluster` living inside the map is resolved automatically once the map has loaded.
+The `StoreLocator` declares no child components — `StoreLocator`, `MapboxMap`, `MapboxCluster`, `MapboxClusterItem` and the optional `MapboxGeocoder` are each [registered](/guide/usage/#registering-components) independently and mount on their own; the orchestrator then discovers them in its subtree once mounted. As with a plain map, the recommended default is to lazy-register each component with js-toolkit's [`importWhen*` helpers](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) and the per-component subpaths (each subpath's default export is the component class), keeping the heavy `mapbox-gl` dependency out of your main bundle until the store locator is on the page.
-Author a root `StoreLocator` element wrapping a sidebar `list` ref of `StoreLocatorItem`s and a `MapboxMap`. The `MapboxCluster` carries **no** authored data — the coordinator derives a GeoJSON `FeatureCollection` from the items and pushes it to the cluster once the map is ready.
+Because a `MapboxClusterItem` resolves its cluster and the cluster resolves its map through the closest matching ancestor, the DOM nests them: the `MapboxCluster` wraps the sidebar list of `MapboxClusterItem`s **inside** the `MapboxMap`, next to the map container. The `StoreLocator` wraps the map. The cluster carries **no** authored data — it derives its source from the registered items.
::: code-group
```js [app.js]
-import { registerComponent } from '@studiometa/js-toolkit';
-import { StoreLocator } from '@studiometa/ui-mapbox';
+import { registerComponents, importWhenVisible } from '@studiometa/js-toolkit';
-registerComponent(StoreLocator);
+// Register only the components your page uses; order doesn't matter.
+registerComponents(
+ importWhenVisible(() => import('@studiometa/ui-mapbox/StoreLocator'), 'StoreLocator'),
+ importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMap'), 'MapboxMap'),
+ importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxCluster'), 'MapboxCluster'),
+ importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxClusterItem'), 'MapboxClusterItem'),
+);
```
```html [index.html]
-
```
@@ -84,23 +97,23 @@ registerComponent(StoreLocator);
@import 'mapbox-gl/dist/mapbox-gl.css';
/* List-visibility contract: hide items outside the current viewport. */
-[data-component='StoreLocatorItem']:not([data-in-bounds]) {
+[data-component='MapboxClusterItem']:not([data-in-bounds]) {
display: none;
}
/* Selected-item contract. */
-[data-component='StoreLocatorItem'][data-active] {
+[data-component='MapboxClusterItem'][data-active] {
/* highlight the active store */
}
```
:::
-The styling contract is entirely data-attribute driven: `data-in-bounds` toggles list visibility, `data-active` (plus `aria-current="true"`) marks the selected item. See the [styling contract](./js-api#styling-contract) for details.
+Clicking anywhere in a `MapboxClusterItem` selects it (the orchestrator delegates the click on its root), and clicking an unclustered pin on the map selects the matching item too. The styling contract is entirely data-attribute driven: `data-in-bounds` toggles list visibility, `data-active` (plus `aria-current="true"`) marks the selected item. See the [styling contract](./js-api#styling-contract) for details.
## Lazy loading
-Registering `StoreLocator` pulls in `MapboxMap` and its heavy `mapbox-gl` dependency (~230 kB gzipped). Register it lazily so it is code-split into its own chunk and only loaded when the locator is on the page, using the same [`importWhen*` helpers](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) as the rest of the [`MapboxMap` family](/components/MapboxMap/#lazy-loading):
+Registering the family pulls in `MapboxMap` and its heavy `mapbox-gl` dependency (~230 kB gzipped). Register it lazily so it is code-split into its own chunk and only loaded when the locator is on the page, using the same [`importWhen*` helpers](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) as the rest of the [`MapboxMap` family](/components/MapboxMap/#lazy-loading):
```js
import { registerComponent, importWhenVisible } from '@studiometa/js-toolkit';
@@ -110,6 +123,6 @@ registerComponent(
);
```
-Every component is also available at its own subpath (`@studiometa/ui-mapbox/`), whose default export is the component class — so the dynamic import needs no destructuring.
+When lazy-loading only the `StoreLocator` subpath, remember to register the `MapboxMap`, `MapboxCluster` and `MapboxClusterItem` it orchestrates too (each is available at its own subpath, whose default export is the component class, so a dynamic import needs no destructuring).
`importWhenIdle`, `importOnInteraction` and `importOnMediaQuery` are available too — see the [MapboxMap lazy-loading note](/components/MapboxMap/#lazy-loading).
diff --git a/packages/docs/components/StoreLocator/js-api.md b/packages/docs/components/StoreLocator/js-api.md
index 1ac559cf..61136b5a 100644
--- a/packages/docs/components/StoreLocator/js-api.md
+++ b/packages/docs/components/StoreLocator/js-api.md
@@ -5,26 +5,48 @@ outline: deep
# JS API
-The store locator is made of two components:
+The store locator is an **orchestrator** built on the [`MapboxMap` family](/components/MapboxMap/js-api):
-- `StoreLocator` — the coordinator, wrapping a [`MapboxMap`](/components/MapboxMap/js-api#map) and the sidebar list.
-- `StoreLocatorItem` — a single store entry living in the sidebar.
+- `StoreLocator` — the coordinator, wrapping a [`MapboxMap`](/components/MapboxMap/js-api#map) that contains a [`MapboxCluster`](/components/MapboxMap/js-api#cluster).
+- [`MapboxClusterItem`](/components/MapboxMap/js-api#mapboxclusteritem) — a single store entry; it registers with the cluster (the owner of the item registry), and the orchestrator reads and drives it. There is **no** dedicated store-item class.
-You only ever register `StoreLocator` with [`registerComponent`](https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html): it declares `MapboxMap` and `StoreLocatorItem` internally, and the optional `MapboxCluster`/`MapboxGeocoder` inside the map are discovered automatically once the map has loaded.
+The `StoreLocator` declares no child components, so nothing is ever double-mounted. [Register](/guide/usage/#registering-components) `StoreLocator`, `MapboxMap`, `MapboxCluster` and `MapboxClusterItem` each independently — ideally behind a lazy [`importWhen*` helper](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) (see [Lazy loading](/components/MapboxMap/#lazy-loading)). The orchestrator discovers them in its subtree with `$query` once mounted, retrying a few ticks for asynchronously-mounted children (the geocoder lazy-imports its module).
-The coordinator reads its options **once, at mount time** — they are **not** reactive, like the rest of the [`MapboxMap` family](/components/MapboxMap/js-api#reactivity-and-updates). To change the store set afterwards, change the DOM of the list (add/remove `StoreLocatorItem` elements) and the coordinator re-derives the map data automatically.
+To change the store set after mount, change the DOM of the list (add/remove `MapboxClusterItem` elements): the cluster re-derives the map data and emits an `update`, and the orchestrator re-fits and re-filters automatically.
## StoreLocator
-The coordinator. It owns no map rendering: it registers the `StoreLocatorItem`s, derives a GeoJSON `FeatureCollection` from them, pushes it to the child `MapboxCluster` once the map is loaded, filters and sorts the list on every map move, and handles selection.
+The coordinator. It owns no map rendering and no registry: the `MapboxCluster` derives the GeoJSON source from its registered `MapboxClusterItem`s, and the orchestrator reads that item set to filter and sort the list on every map move, wire the geocoder, re-frame the map on item-set changes, and handle selection.
### Options
-| Option | Type | Default | Description |
-| ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------- |
-| `item-zoom-level` | `Number` | `14` | The zoom level the map flies to when a store is selected. |
-| `no-sort` | `Boolean` | `false` | Disable the distance sort. By default the in-view items are reordered nearest-first on every map move. |
-| `fit-on-update` | `Boolean` | `false` | Fit the map bounds to the whole item set whenever it changes (on mount and on any add/remove of items). |
+#### `item-zoom-level`
+
+- Type: `Number`
+- Default: `14`
+
+The zoom level the map flies to when a store is selected.
+
+#### `no-sort`
+
+- Type: `Boolean`
+- Default: `false`
+
+Disable the distance sort. Sorting is on by default, reordering the in-view items nearest-first on every map move, so add `data-option-no-sort` to turn it off.
+
+#### `fit-on-update`
+
+- Type: `Boolean`
+- Default: `false`
+
+Fit the map bounds to the whole item set whenever it changes (on load and on any add/remove of items).
+
+#### `popup-options`
+
+- Type: `Object`
+- Default: `{}`
+
+Options forwarded to the `mapboxgl.Popup` opened on selection.
```html {3}
@@ -37,46 +59,61 @@ The coordinator. It owns no map rendering: it registers the `StoreLocatorItem`s,
```
-### Refs
+### Getters
+
+#### `isLoaded`
-| Ref | Type | Description |
-| ------ | ------------- | --------------------------------------------------------------------------------------------------------- |
-| `list` | `HTMLElement` | The sidebar container holding the `StoreLocatorItem`s. The coordinator reorders its children by distance. |
+- Type: `boolean`
-### Getters
+Whether the underlying map has finished loading (a public field, not a getter).
-| Getter | Type | Description |
-| ------------------- | ----------------------------- | ------------------------------------------------------------------------------- |
-| `isLoaded` | `boolean` | Whether the underlying map has finished loading. (a public field, not a getter) |
-| `mapboxMap` | `MapboxMap` | The closest child `MapboxMap` component. |
-| `map` | `mapboxgl.Map` | The underlying Mapbox `Map` instance. Only valid once the map has loaded. |
-| `cluster` | `MapboxCluster \| undefined` | The optional `MapboxCluster` child, mounted by the `MapboxMap`. |
-| `geocoder` | `MapboxGeocoder \| undefined` | The optional `MapboxGeocoder` child, mounted by the `MapboxMap`. |
-| `featureCollection` | `FeatureCollection` | The GeoJSON derived from the registered items — the data pushed to the source. |
+#### `mapboxMap`
-### Methods
+- Type: `MapboxMap`
-#### `selectItem(item)`
+The child `MapboxMap` component.
-- Arguments: `StoreLocatorItem`
+#### `map`
-Select a store: deactivate the previous one, fly the map to the item at `item-zoom-level`, mark it active (`data-active` + `aria-current="true"`) and emit [`select`](#select). Called by an item's own click, by a cluster feature-click, and available for you to call directly.
+- Type: `mapboxgl.Map`
-#### `deselect()`
+The underlying Mapbox `Map` instance. Only valid once the map has loaded.
+
+#### `cluster`
+
+- Type: `MapboxCluster | undefined`
+
+The child `MapboxCluster` (the item registry + map data source).
+
+#### `geocoder`
+
+- Type: `MapboxGeocoder | undefined`
+
+The optional child `MapboxGeocoder`.
+
+#### `items`
-Clear the current selection, remove the active state and emit [`deselect`](#deselect).
+- Type: `MapboxClusterItem[]`
-#### `registerItem(item)` / `unregisterItem(item)`
+The registered items, read from the cluster (their single source of truth).
-- Arguments: `StoreLocatorItem`
+### Methods
+
+#### `selectItem(item)`
+
+- Arguments: `MapboxClusterItem`
+
+Select a store: deactivate the previous one, fly the map to the item at `item-zoom-level`, open a popup from the item's [`popupContent`](/components/MapboxMap/js-api#mapboxclusteritem), mark it active (`data-active` + `aria-current="true"`) and emit [`select`](#select). Called automatically on a sidebar click (delegated on the root), on the cluster's `item-click` (an unclustered pin), and available for you to call directly.
+
+#### `deselect()`
-Add/remove an item from the coordinator's registry and schedule a coalesced map-data sync. These are called automatically by the `StoreLocatorItem`s on mount/destroy — you rarely call them yourself, but they are the hook a `Fetch` list swap relies on.
+Clear the current selection, close the popup, remove the active state and emit [`deselect`](#deselect).
### Events
#### `select`
-Emitted when a store is selected, with the selected `StoreLocatorItem` instance. Wire your detail panel here.
+Emitted when a store is selected, with the selected `MapboxClusterItem` instance. Wire your detail panel here.
```js
onStoreLocatorSelect({ args: [item] }) {
@@ -98,64 +135,27 @@ onStoreLocatorFilter({ args: [items] }) {
}
```
-## StoreLocatorItem
-
-A single store entry. Unlike the other `@studiometa/ui-mapbox` components, it does **not** live inside the map: its DOM is in the sidebar list, and its context is the parent `StoreLocator` (resolved via `$closest('StoreLocator')`). It is headless — the coordinator only reflects state as data-attributes so you can style it with plain CSS.
-
-### Options
-
-| Option | Type | Default | Description |
-| --------- | -------- | -------- | ------------------------------------------------------------------------ |
-| `id` | `String` | — | Stable identifier, used to match a clicked map feature back to the item. |
-| `lng-lat` | `Array` | `[0, 0]` | The store coordinates as `[longitude, latitude]`. |
-
-### Refs
-
-| Ref | Type | Description |
-| -------- | ------------- | ------------------------------------------------------------------------------------------- |
-| `select` | `HTMLElement` | The element (typically a `