@kahwee/sf-map-svg 4.3.2 → 4.5.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +18 -1
  3. package/dist/src/cards.d.ts +15 -0
  4. package/dist/src/cards.js +138 -0
  5. package/dist/src/cluster-layer.d.ts +2 -0
  6. package/dist/src/cluster-layer.js +2 -0
  7. package/dist/src/explorer-controls.d.ts +26 -0
  8. package/dist/src/explorer-controls.js +120 -0
  9. package/dist/src/explorer-core.d.ts +6 -1
  10. package/dist/src/explorer-core.js +168 -701
  11. package/dist/src/explorer-shell.d.ts +62 -0
  12. package/dist/src/explorer-shell.js +228 -0
  13. package/dist/src/explorer-sidebar.d.ts +20 -0
  14. package/dist/src/explorer-sidebar.js +79 -0
  15. package/dist/src/guide-data.js +1 -0
  16. package/dist/src/guide-map.js +2 -1
  17. package/dist/src/guide-static.js +1 -0
  18. package/dist/src/label-candidates.d.ts +24 -0
  19. package/dist/src/label-candidates.js +29 -0
  20. package/dist/src/label-renderer.d.ts +1 -10
  21. package/dist/src/label-spread.d.ts +5 -0
  22. package/dist/src/label-spread.js +36 -0
  23. package/dist/src/map-core.d.ts +3 -1
  24. package/dist/src/map-core.js +7 -5
  25. package/dist/src/map.js +2 -1
  26. package/dist/src/neighborhood-layer.d.ts +67 -0
  27. package/dist/src/neighborhood-layer.js +256 -0
  28. package/dist/src/overlay-layer.d.ts +13 -0
  29. package/dist/src/overlay-layer.js +68 -0
  30. package/dist/src/palettes.d.ts +48 -0
  31. package/dist/src/palettes.js +40 -0
  32. package/dist/src/presets.d.ts +1 -0
  33. package/dist/src/presets.js +1 -0
  34. package/dist/src/static-labels.d.ts +7 -0
  35. package/dist/src/static-labels.js +115 -0
  36. package/dist/src/static.js +31 -3
  37. package/dist/src/types.d.ts +16 -1
  38. package/docs/API.md +38 -2
  39. package/docs/EXAMPLES.md +1 -0
  40. package/docs/consumer-integration.md +81 -1
  41. package/docs/guide-bundle-report.md +4 -4
  42. package/llms-full.txt +274 -26
  43. package/llms.txt +1 -1
  44. package/package.json +16 -8
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 4.5.0 — 2026-10-14
8
+
9
+ - Add the optional `/cards` entry with `bindMapCards()`: hover/focus previews a pin without changing selection or camera, native buttons select places, map selection reveals matching cards without stealing focus, and explicit cleanup restores owned DOM attributes.
10
+ - Spread budgeted static labels across the map while favoring nearby-pin context and retaining a selected-place label when it fits. Keep collision avoidance, all pins, unbudgeted labels, and default output unchanged.
11
+
12
+ - Add opt-in `mapPalettes.nightForest` and `mapPalettes.warmPaper` tokens for consistent static/interactive map colors, label halos, markers, and selection rings, with contrast checks.
13
+
14
+ - Port YorkSF’s editorial map-preview pattern into static rendering: opt-in compact SVG label layouts, spatial label budgets, and configurable halos for dark palettes. Add a build-time field-guide example and light/dark Storybook preview without runtime dependencies or changes to default output.
15
+
16
+ - Separate interactive neighborhood state, controls, sidebar downloads and overlays into lifecycle-owned modules; revoke neighborhood hover listeners when replacing sources and keep optional explorer sidebar code out of normal map bundles.
17
+ - Build the package once during `pnpm check`, while keeping standalone test and typecheck commands self-contained.
18
+
19
+ - Include compatible development-tool updates to Vite 8.3.4 and Playwright 1.64.0. Keep Storybook/addons 10.6.1 and aligned Vitest/browser/coverage 5.0.3; their newer 10.6.2 and 5.0.4 releases remain inside pnpm's release-age window. Make the consumer examples' custom touch-button accessible names include their visible text. Remove the obsolete Storybook 10.6.0 peer overrides; retain Node 24+ and zero runtime dependencies. These tooling changes were previously prepared as 4.4.1 but not published separately.
20
+
21
+ ## 4.4.0 — 2026-10-12
22
+
23
+ - Add opt-in static `labelLayout: { width }` to `renderMap`, `createGuideSVG`, and `createGuideShell`. Static and interactive maps now share overview priorities, neighborhood anchors, font sizes, and collision rules; centered labels retain their anchors during enhancement.
24
+ - Add static `neighborhoodLabels`, available flat or under `layers`, for the new layout. Guide SVGs and shells enable neighborhood names when opting in. Existing fixed-label output and interactive defaults remain unchanged; `labels: false` still hides text while preserving symbols and accessible titles.
25
+ - Keep static layout server-safe and dependency-free. The width is a CSS canvas-width hint, not the SVG projection width; portable font estimates can produce a different visible label set from browser-measured fonts. Document matching data, markers, colors, and layers, plus responsive-width and custom-style limitations.
26
+ - Keep server font estimation out of interactive-only imports and browser runtime code out of static imports. Refresh bundle reports and enforce bounded size budgets for the combined renderer and static-capable Pages tools.
27
+ - Add desktop and phone enhancement stories, collision and input-validation regressions, an SVG example, and copyable integration documentation. Existing screenshot baselines remain unchanged.
28
+
7
29
  ## 4.3.2 — 2026-10-10
8
30
 
9
31
  - Update compatible development tooling and refresh dependency security fixes.
package/README.md CHANGED
@@ -33,6 +33,23 @@ const { svg, project, viewBox } = renderMap(staticMapData, {
33
33
 
34
34
  See [map options and animations](docs/API.md#static-maps) for optional layers and static animation.
35
35
 
36
+ ## Labels before JavaScript loads
37
+
38
+ Version 4.4 adds an opt-in overview layout for server-rendered guides:
39
+
40
+ ```ts
41
+ import { createGuideShell } from '@kahwee/sf-map-svg/guide/static';
42
+
43
+ const guideMarkup = createGuideShell({
44
+ title: 'Explore San Francisco',
45
+ labelLayout: { width: 390 },
46
+ });
47
+ ```
48
+
49
+ `width` is the expected canvas width in CSS pixels. The SVG keeps its 800 × 800 projection while labels share the interactive map's priorities, anchors, sizes and collision rules. Guide shells include neighborhood names in this layout; calls without `labelLayout` keep their previous output.
50
+
51
+ Enhance the shell with `mountGuideController()` using the same data, colors, markers and layers. Static font widths are estimated, so browser fonts and resize can change which labels fit. See the [complete integration recipe and width guidance](docs/consumer-integration.md#keep-overview-labels-consistent). No migration is required from 4.3.
52
+
36
53
  ## Interactive map
37
54
 
38
55
  ```ts
@@ -165,7 +182,7 @@ pnpm benchmark:markers --counts=2000 --browsers=chromium,firefox,webkit \
165
182
 
166
183
  ### Cached label preparation and renderer components
167
184
 
168
- Neighborhood priority is sorted once per source. Styled label candidates are reused until selection, catalog, source, layers, appearance, or zoom/width eligibility changes. Overlay bounds are measured before marker and cluster SVG updates; secondary-road visibility and scale furniture skip unchanged writes. Marker catalog reconciliation and cluster rendering now have separate modules, reducing the explorer controller from 2,316 to 2,151 lines. The interactive renderer is 34.6 KiB gzip, up from 34.2 KiB, within the unchanged 35 KiB limit; static rendering remains 7.9 KiB.
185
+ Neighborhood priority is sorted once per source. Styled label candidates are reused until selection, catalog, source, layers, appearance, or zoom/width eligibility changes. Overlay bounds are measured before marker and cluster SVG updates; secondary-road visibility and scale furniture skip unchanged writes. Marker catalog reconciliation and cluster rendering now have separate modules, reducing the explorer controller from 2,316 to 2,151 lines. At the 4.3.2 marker-performance baseline, the renderer was 34.6 KiB gzip and static rendering was 7.9 KiB. See the [current bundle report](docs/guide-bundle-report.md) for measurements including the opt-in static label layout.
169
186
 
170
187
  A fresh Chromium comparison against `e774fbd` used 2,000 labeled markers, 20 operation samples, both widths, and all six frame workloads in basemap and neighborhood modes. Neighborhood-mode frame interval p95 values (milliseconds):
171
188
 
@@ -0,0 +1,15 @@
1
+ import type { MapController } from './controller-types.js';
2
+ import type { MapMarker } from './types.js';
3
+ export interface MapCardsOptions {
4
+ markers: readonly MapMarker[];
5
+ highlightColor?: string;
6
+ /** Reveal selected cards with native scrolling, without moving keyboard focus. Default true. */
7
+ scrollOnSelect?: boolean;
8
+ }
9
+ /** Connect consumer-owned [data-map-marker] cards and [data-map-select] buttons to a map. */
10
+ export declare function bindMapCards(map: MapController, root: HTMLElement, options: MapCardsOptions): {
11
+ /** Call after map.setMarkers() when your place catalog changes. */
12
+ setMarkers(next: readonly MapMarker[]): void;
13
+ /** Remove owned listeners/decoration and restore card attributes. Call before map.destroy(). */
14
+ destroy(): void;
15
+ };
@@ -0,0 +1,138 @@
1
+ import { validateMarkers } from './validation.js';
2
+ /** Connect consumer-owned [data-map-marker] cards and [data-map-select] buttons to a map. */
3
+ export function bindMapCards(map, root, options) {
4
+ if (map.destroyed)
5
+ throw new Error('Cannot bind cards to a destroyed map.');
6
+ validateMarkers(options.markers);
7
+ let markers = new Map(options.markers.map((marker) => [marker.id, { ...marker }]));
8
+ let disposed = false;
9
+ let hovered = null;
10
+ let focused = null;
11
+ let selecting = false;
12
+ const document = root.ownerDocument;
13
+ const ring = document.createElement('span');
14
+ ring.dataset.mapCardHighlight = '';
15
+ ring.setAttribute('aria-hidden', 'true');
16
+ ring.style.cssText =
17
+ 'position:absolute;width:26px;height:26px;box-sizing:border-box;border:3px solid;border-radius:50%;transform:translate(-50%,-50%);pointer-events:none';
18
+ ring.style.borderColor = options.highlightColor ?? '#e4531b';
19
+ ring.hidden = true;
20
+ map.overlayElement.append(ring);
21
+ const originals = new Map();
22
+ function attribute(node, key, value) {
23
+ let saved = originals.get(node);
24
+ if (!saved) {
25
+ saved = new Map();
26
+ originals.set(node, saved);
27
+ }
28
+ if (!saved.has(key))
29
+ saved.set(key, node.getAttribute(key));
30
+ if (value === null)
31
+ node.removeAttribute(key);
32
+ else
33
+ node.setAttribute(key, value);
34
+ }
35
+ const cardAt = (target) => {
36
+ const card = target instanceof Element ? target.closest('[data-map-marker]') : null;
37
+ return card && root.contains(card) ? card : null;
38
+ };
39
+ function highlight() {
40
+ ring.hidden = true;
41
+ if (disposed || map.destroyed || !map.overlayElement.getBoundingClientRect().width)
42
+ return;
43
+ const card = focused ?? hovered;
44
+ const marker = card && root.contains(card) ? markers.get(card.dataset.mapMarker ?? '') : undefined;
45
+ if (!marker)
46
+ return;
47
+ const point = map.projectToScreen(marker.lng, marker.lat);
48
+ if (!point.visible)
49
+ return;
50
+ ring.style.left = `${point.x}px`;
51
+ ring.style.top = `${point.y}px`;
52
+ ring.hidden = false;
53
+ }
54
+ function selection(id, reveal) {
55
+ for (const card of root.querySelectorAll('[data-map-marker]')) {
56
+ const selected = id !== null && card.dataset.mapMarker === id;
57
+ attribute(card, 'data-map-selected', selected ? '' : null);
58
+ for (const button of card.querySelectorAll('button[data-map-select]'))
59
+ attribute(button, 'aria-pressed', String(selected));
60
+ if (selected && reveal && !selecting && options.scrollOnSelect !== false)
61
+ card.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' });
62
+ }
63
+ }
64
+ const handlers = {
65
+ pointerover: (event) => {
66
+ if (event.pointerType !== 'touch') {
67
+ hovered = cardAt(event.target);
68
+ highlight();
69
+ }
70
+ },
71
+ pointerout: (event) => {
72
+ hovered = cardAt(event.relatedTarget);
73
+ highlight();
74
+ },
75
+ focusin: (event) => {
76
+ focused = cardAt(event.target);
77
+ highlight();
78
+ },
79
+ focusout: (event) => {
80
+ focused = cardAt(event.relatedTarget);
81
+ highlight();
82
+ },
83
+ click: (event) => {
84
+ if (disposed || map.destroyed)
85
+ return;
86
+ const target = event.target instanceof Element ? event.target.closest('button[data-map-select]') : null;
87
+ const card = cardAt(target);
88
+ const id = card?.dataset.mapMarker;
89
+ if (!target || !card || !id || !markers.has(id) || target.disabled)
90
+ return;
91
+ selecting = true;
92
+ try {
93
+ map.selectMarker(id, { fit: false });
94
+ }
95
+ finally {
96
+ selecting = false;
97
+ }
98
+ },
99
+ };
100
+ for (const [type, listener] of Object.entries(handlers))
101
+ root.addEventListener(type, listener);
102
+ const unsubscribers = [
103
+ map.on('markerchange', ({ id }) => selection(id, true)),
104
+ map.on('viewportchange', highlight),
105
+ map.on('mapresize', highlight),
106
+ ];
107
+ selection(map.getSelectedMarker()?.id ?? null, false);
108
+ return {
109
+ /** Call after map.setMarkers() when your place catalog changes. */
110
+ setMarkers(next) {
111
+ if (disposed)
112
+ return;
113
+ validateMarkers(next);
114
+ markers = new Map(next.map((marker) => [marker.id, { ...marker }]));
115
+ if (!map.destroyed)
116
+ selection(map.getSelectedMarker()?.id ?? null, false);
117
+ highlight();
118
+ },
119
+ /** Remove owned listeners/decoration and restore card attributes. Call before map.destroy(). */
120
+ destroy() {
121
+ if (disposed)
122
+ return;
123
+ disposed = true;
124
+ for (const [type, listener] of Object.entries(handlers))
125
+ root.removeEventListener(type, listener);
126
+ for (const unsubscribe of unsubscribers)
127
+ unsubscribe();
128
+ ring.remove();
129
+ for (const [node, saved] of originals)
130
+ for (const [key, value] of saved)
131
+ if (value === null)
132
+ node.removeAttribute(key);
133
+ else
134
+ node.setAttribute(key, value);
135
+ originals.clear();
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,2 @@
1
+ /** Lifecycle-owned cluster rendering, including lazy markers and viewport mounting. */
2
+ export { createClusterRenderer as createClusterLayer } from './cluster-renderer.js';
@@ -0,0 +1,2 @@
1
+ /** Lifecycle-owned cluster rendering, including lazy markers and viewport mounting. */
2
+ export { createClusterRenderer as createClusterLayer } from './cluster-renderer.js';
@@ -0,0 +1,26 @@
1
+ import type { createExplorerShell } from './explorer-shell.js';
2
+ import type { MapViewport, NeighborhoodExplorerElement, NeighborhoodExplorerOptions } from './types.js';
3
+ type Controls = NonNullable<NeighborhoodExplorerOptions['controls']>;
4
+ type Shell = Pick<ReturnType<typeof createExplorerShell>, 'root' | 'toolbar' | 'canvas' | 'hint' | 'status' | 'modeSelect' | 'sourceSelect' | 'search' | 'results' | 'neighborhoodSelect' | 'markerSelect'>;
5
+ type Commands = Pick<NeighborhoodExplorerElement, 'getViewport' | 'zoomBy' | 'resetView' | 'setLabels' | 'stopAnimation' | 'setMode' | 'setSource' | 'selectNeighborhood' | 'selectMarker'> & {
6
+ svg: SVGSVGElement;
7
+ signal: AbortSignal;
8
+ strings: NonNullable<NeighborhoodExplorerOptions['strings']>;
9
+ isDestroyed(): boolean;
10
+ getLabels(): boolean;
11
+ moveView(view: MapViewport): void;
12
+ setView(view: MapViewport): void;
13
+ updateResults(): void;
14
+ search(query: string): string | undefined;
15
+ };
16
+ /** Own toolbar and form events, touch navigation and control visibility.
17
+ * Commands read current map state; all listeners share the map's disposal signal.
18
+ */
19
+ export declare function createExplorerControls({ root, toolbar, canvas, hint, status, modeSelect, sourceSelect, search, results, neighborhoodSelect, markerSelect, }: Shell, { svg, signal, strings, isDestroyed, getLabels, getViewport, moveView, zoomBy, resetView, setLabels, stopAnimation, setView, setMode, setSource, selectNeighborhood, selectMarker, updateResults, search: findNeighborhood, }: Commands): {
20
+ setTouchNavigation: (enabled: boolean) => void;
21
+ cancelNavigation: () => void;
22
+ update(controls: Controls): void;
23
+ updateLabels(labels: boolean): void;
24
+ updateZoom(zoom: number): void;
25
+ };
26
+ export {};
@@ -0,0 +1,120 @@
1
+ import { element } from './dom.js';
2
+ import { attachNavigation } from './navigation.js';
3
+ /** Own toolbar and form events, touch navigation and control visibility.
4
+ * Commands read current map state; all listeners share the map's disposal signal.
5
+ */
6
+ export function createExplorerControls({ root, toolbar, canvas, hint, status, modeSelect, sourceSelect, search, results, neighborhoodSelect, markerSelect, }, { svg, signal, strings, isDestroyed, getLabels, getViewport, moveView, zoomBy, resetView, setLabels, stopAnimation, setView, setMode, setSource, selectNeighborhood, selectMarker, updateResults, search: findNeighborhood, }) {
7
+ function listen(node, event, handler) {
8
+ node.addEventListener(event, (incoming) => handler(incoming), { signal });
9
+ }
10
+ const zoomControls = element('div', '', 'sf-explorer-control-group');
11
+ const panControls = element('div', '', 'sf-explorer-control-group sf-explorer-pan-controls');
12
+ toolbar.append(zoomControls, panControls);
13
+ function button(text, label, action, group = zoomControls) {
14
+ const node = element('button', text);
15
+ node.type = 'button';
16
+ node.setAttribute('aria-label', label);
17
+ node.addEventListener('click', () => action(), { signal });
18
+ group.append(node);
19
+ return node;
20
+ }
21
+ const zoomOut = button('−', 'Zoom out', () => zoomBy(1 / 1.5));
22
+ const zoomText = element('span', '100%', 'sf-explorer-zoom');
23
+ zoomControls.append(zoomText);
24
+ const zoomIn = button('+', 'Zoom in', () => zoomBy(1.5));
25
+ const resetButton = button(strings.reset ?? 'Reset', strings.reset ?? 'Reset map to city view', resetView);
26
+ const labelsButton = button('Labels', 'Show map labels', () => setLabels(!getLabels()));
27
+ for (const [symbol, label, dx, dy] of [
28
+ ['←', 'Pan west', -1, 0],
29
+ ['↑', 'Pan north', 0, -1],
30
+ ['↓', 'Pan south', 0, 1],
31
+ ['→', 'Pan east', 1, 0],
32
+ ])
33
+ button(symbol, label, () => {
34
+ const view = getViewport();
35
+ moveView([view[0] + (dx * view[2]) / 4, view[1] + (dy * view[2]) / 4, view[2]]);
36
+ }, panControls);
37
+ const touchLabel = strings.touchNavigation ?? 'Touch navigation';
38
+ const touchButton = button(touchLabel, strings.touchNavigationLabel ?? `Enable ${touchLabel.toLowerCase()}`, () => setTouchNavigation(!touchNavigation));
39
+ let touchNavigation = false;
40
+ const navigation = attachNavigation(svg, getViewport, (next) => {
41
+ stopAnimation();
42
+ setView(next);
43
+ }, signal, () => setTouchNavigation(false));
44
+ function setTouchNavigation(enabled) {
45
+ if (isDestroyed())
46
+ return;
47
+ if (typeof enabled !== 'boolean')
48
+ throw new TypeError('Touch navigation must be a boolean.');
49
+ touchNavigation = enabled;
50
+ navigation.setTouchNavigation(enabled);
51
+ canvas.style.touchAction = enabled ? 'none' : 'pan-y pinch-zoom';
52
+ touchButton.textContent = enabled
53
+ ? (strings.touchNavigationDone ?? 'Done: page scrolling')
54
+ : touchLabel;
55
+ touchButton.setAttribute('aria-label', enabled
56
+ ? (strings.touchNavigationExitLabel ?? `Exit ${touchLabel.toLowerCase()}`)
57
+ : (strings.touchNavigationLabel ?? `Enable ${touchLabel.toLowerCase()}`));
58
+ touchButton.setAttribute('aria-pressed', String(enabled));
59
+ root.dataset.touchNavigation = String(enabled);
60
+ }
61
+ listen(modeSelect, 'change', () => {
62
+ const next = modeSelect.value;
63
+ if (next === 'neighborhoods' || next === 'districts' || next === 'basemap')
64
+ setMode(next);
65
+ });
66
+ listen(search, 'input', updateResults);
67
+ listen(search, 'keydown', (event) => {
68
+ if (event.key === 'Enter') {
69
+ const match = findNeighborhood(search.value);
70
+ if (match) {
71
+ event.preventDefault();
72
+ selectNeighborhood(match);
73
+ }
74
+ }
75
+ });
76
+ listen(sourceSelect, 'change', () => {
77
+ const next = sourceSelect.value;
78
+ if (next === 'realtor' || next === 'sf-find' || next === 'analysis')
79
+ setSource(next);
80
+ });
81
+ listen(results, 'click', (event) => {
82
+ const node = event.target instanceof Element
83
+ ? event.target.closest('[data-neighborhood-result]')
84
+ : null;
85
+ if (node?.dataset.neighborhoodResult)
86
+ selectNeighborhood(node.dataset.neighborhoodResult);
87
+ });
88
+ listen(neighborhoodSelect, 'change', () => selectNeighborhood(neighborhoodSelect.value || null));
89
+ listen(markerSelect, 'change', () => selectMarker(markerSelect.value || null));
90
+ return {
91
+ setTouchNavigation,
92
+ cancelNavigation: navigation.cancel,
93
+ update(controls) {
94
+ for (const node of [zoomIn, zoomOut, zoomText])
95
+ node.hidden = controls.zoom === false;
96
+ panControls.hidden = controls.pan === false;
97
+ resetButton.hidden = controls.reset === false;
98
+ labelsButton.hidden = controls.labels === false;
99
+ touchButton.hidden = controls.touch === false;
100
+ if (touchButton.hidden && touchNavigation)
101
+ setTouchNavigation(false);
102
+ zoomControls.hidden = [...zoomControls.children].every((node) => node.hidden);
103
+ toolbar.hidden = zoomControls.hidden && panControls.hidden;
104
+ hint.hidden = controls.help === false;
105
+ if (hint.hidden)
106
+ svg.setAttribute('aria-describedby', hint.id);
107
+ else
108
+ svg.removeAttribute('aria-describedby');
109
+ status.classList.toggle('sf-explorer-visually-hidden', controls.status === false);
110
+ },
111
+ updateLabels(labels) {
112
+ labelsButton.setAttribute('aria-pressed', String(labels));
113
+ },
114
+ updateZoom(zoom) {
115
+ zoomText.textContent = `${Math.round(zoom * 100)}%`;
116
+ zoomIn.disabled = zoom >= 12;
117
+ zoomOut.disabled = zoom <= 1;
118
+ },
119
+ };
120
+ }
@@ -1,6 +1,8 @@
1
1
  import type { NeighborhoodSource } from '../data/types.js';
2
2
  import type { MapConfiguration } from './controller-types.js';
3
3
  import type { InteractiveSFMapData } from './explorer-data.js';
4
+ import type { createMapShell } from './explorer-shell.js';
5
+ import type { createExplorerSidebar } from './explorer-sidebar.js';
4
6
  import type { DistrictYear, ExplorerMode, NeighborhoodExplorerElement, NeighborhoodExplorerOptions } from './types.js';
5
7
  export type { ExplorerMode, NeighborhoodExplorerElement, NeighborhoodExplorerOptions, } from './types.js';
6
8
  /** Private bridge between the renderer and its public lifecycle controller. */
@@ -14,4 +16,7 @@ type MapEngineElement = NeighborhoodExplorerElement & {
14
16
  };
15
17
  };
16
18
  /** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
17
- export declare function createNeighborhoodExplorerCore(options: NeighborhoodExplorerOptions | undefined, data: InteractiveSFMapData): MapEngineElement;
19
+ export declare function createNeighborhoodExplorerCore(options: NeighborhoodExplorerOptions | undefined, data: InteractiveSFMapData, ui: {
20
+ createShell: typeof createMapShell;
21
+ createSidebar?: typeof createExplorerSidebar;
22
+ }): MapEngineElement;