Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 6.1.1 (UNRELEASED)

- Reader pan: Native gesture detection across Apple + Google Maps.
- Fit bounds: Jump searches now frame result pins; area searches stay in place.
- Theme sync: Maps now follow the app/system light/dark theme.

## 6.1.0 (August 31, 2026)

- Meetings now use Apple Maps on iPhone and iPad.
Expand Down
12 changes: 7 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ export default ts.config(
globals: {
...globals.browser,
...globals.node,
// The Maps SDK namespace, from @types/google.maps. Declared here because
// `no-undef` cannot see a TypeScript type annotation as anything other
// than an undefined identifier, and it is a genuine runtime global once
// the SDK has loaded.
google: 'readonly'
// `google` is the ambient Google Maps namespace from @types/google.maps. It
// is used in type positions only, but ESLint cannot tell those apart.
// `__GIT_SHA__` is substituted by Vite's `define` at build time, so it is a
// real global at runtime but exists in no file for ESLint to find.
google: 'readonly',
__GIT_SHA__: 'readonly',
__APP_VERSION__: 'readonly'
}
}
},
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bmlt-search",
"private": true,
"version": "6.1.0",
"version": "6.1.1",
"license": "MIT",
"type": "module",
"author": "BMLT Enabled",
Expand Down Expand Up @@ -96,6 +96,6 @@
"@capacitor/status-bar": "^8.0.3",
"@googlemaps/js-api-loader": "^2.1.1",
"@lucide/svelte": "^1.37.0",
"capacitor-plugin-apple-maps": "^0.3.4"
"capacitor-plugin-apple-maps": "^0.5.0"
}
}
6 changes: 6 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ declare global {
// interface Platform {}
}

/** Short commit this bundle was built from; see `gitCommit` in vite.config.ts. */
const __GIT_SHA__: string;

/** Release version, from the git tag; see `appVersion` in vite.config.ts. */
const __APP_VERSION__: string;

/**
* The Maps keys, read from `import.meta.env` rather than `$env/static/public`
* — see src/lib/maps/keys.ts for why.
Expand Down
49 changes: 47 additions & 2 deletions src/lib/maps/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GoogleMap } from '@capacitor/google-maps';
import { GoogleMap, LatLngBounds } from '@capacitor/google-maps';
import { AppleMap } from 'capacitor-plugin-apple-maps';
import { platform } from '../native';
import { mapKey } from './keys';
Expand Down Expand Up @@ -58,22 +58,56 @@ export interface ProviderMarker {
export interface CreateMapOptions {
id: string;
element: HTMLElement;
config: { center: LatLng; zoom: number; minZoom?: number };
config: { center: LatLng; zoom: number; minZoom?: number; colorScheme?: 'light' | 'dark' };
}

/** Provider-neutral handle over the native map, exposing only what the route needs. */
export interface MapHandle {
setOnCameraIdleListener(callback: (data: CameraIdleData) => void): Promise<void>;
/**
* Fires once as the camera starts moving, before the ensuing idle, with the
* plugin's authoritative `isGesture` flag: true for a user pan/zoom, false for
* a programmatic move (our own `setCamera`/`fitBounds`, or the provider
* recentring itself on a marker tap). Both plugins expose it identically.
*/
setOnCameraMoveStartedListener(callback: (isGesture: boolean) => void): Promise<void>;
setOnMarkerClickListener(callback: (data: MarkerClickData) => void): Promise<void>;
getMapBounds(): Promise<ProviderBounds>;
setCamera(config: { coordinate?: LatLng; zoom?: number }): Promise<void>;
/** Frame the camera to enclose every coordinate. `padding` is an edge inset in pixels. */
fitBounds(coordinates: LatLng[], padding?: number): Promise<void>;
/** Match the map's appearance to a light/dark scheme. No-op where the provider has no such control (Google). */
setColorScheme(scheme: 'light' | 'dark'): Promise<void>;
addMarkers(markers: ProviderMarker[]): Promise<string[]>;
removeMarkers(ids: string[]): Promise<void>;
enableClustering(): Promise<void>;
disableClustering(): Promise<void>;
destroy(): Promise<void>;
}

/**
* Bounding box of a set of coordinates, in the {southwest, center, northeast}
* shape Google's `fitBounds` wants. Apple's `fitBounds` takes the raw `LatLng[]`
* and computes this itself, so this is only needed on the Google branch.
*/
function boundsOf(coordinates: LatLng[]): ProviderBounds {
let minLat = Infinity;
let maxLat = -Infinity;
let minLng = Infinity;
let maxLng = -Infinity;
for (const { lat, lng } of coordinates) {
minLat = Math.min(minLat, lat);
maxLat = Math.max(maxLat, lat);
minLng = Math.min(minLng, lng);
maxLng = Math.max(maxLng, lng);
}
return {
southwest: { lat: minLat, lng: minLng },
northeast: { lat: maxLat, lng: maxLng },
center: { lat: (minLat + maxLat) / 2, lng: (minLng + maxLng) / 2 }
};
}

/** Normalise either provider's bounds object into {center, southwest, northeast}. */
function normaliseBounds(bounds: ProviderBounds): ProviderBounds {
return {
Expand Down Expand Up @@ -104,9 +138,14 @@ export async function createMap(options: CreateMapOptions): Promise<MapHandle> {
});
return {
setOnCameraIdleListener: (cb) => map.setOnCameraIdleListener((d) => cb({ latitude: d.latitude, longitude: d.longitude, zoom: d.zoom, bounds: normaliseBounds(d.bounds) })),
setOnCameraMoveStartedListener: (cb) => map.setOnCameraMoveStartedListener((d) => cb(d.isGesture)),
setOnMarkerClickListener: (cb) => map.setOnMarkerClickListener((d) => cb({ markerId: d.markerId })),
getMapBounds: async () => normaliseBounds(await map.getMapBounds()),
setCamera: (config) => map.setCamera(config),
// Apple's fitBounds takes the raw coordinates and frames them to the real
// viewport aspect ratio (setVisibleMapRect(_:edgePadding:)).
fitBounds: (coordinates, padding) => map.fitBounds(coordinates, padding),
setColorScheme: (scheme) => map.setColorScheme(scheme),
// MapKit sizes the annotation image; without a size the raw PNG pixels are
// used, which is tiny on a hi-DPI screen. 60×72 keeps the pin art's ~0.83
// aspect (the source is 83×100) and matches the marker size in the
Expand All @@ -130,9 +169,15 @@ export async function createMap(options: CreateMapOptions): Promise<MapHandle> {
return {
setOnCameraIdleListener: (cb) =>
map.setOnCameraIdleListener((d) => cb({ latitude: d.latitude, longitude: d.longitude, zoom: d.zoom, bounds: normaliseBounds(d.bounds as unknown as ProviderBounds) })),
setOnCameraMoveStartedListener: (cb) => map.setOnCameraMoveStartedListener((d) => cb(d.isGesture)),
setOnMarkerClickListener: (cb) => map.setOnMarkerClickListener((d) => cb({ markerId: d.markerId })),
getMapBounds: async () => normaliseBounds((await map.getMapBounds()) as unknown as ProviderBounds),
setCamera: (config) => map.setCamera(config),
// Google's fitBounds wants a LatLngBounds; build it from the pins' box.
fitBounds: (coordinates, padding) => map.fitBounds(new LatLngBounds(boundsOf(coordinates)), padding),
// Google Maps has no runtime light/dark toggle here; the app follows the
// system scheme via CSS on the web/Android paths.
setColorScheme: () => Promise.resolve(),
// The Google pin art is anchored by its tip: half its width across, its full
// height down — the value the route used before this abstraction existed.
addMarkers: (markers) => map.addMarkers(markers.map((m) => ({ coordinate: m.coordinate, iconUrl: m.iconUrl, iconAnchor: { x: 15, y: 45 } }))),
Expand Down
2 changes: 2 additions & 0 deletions src/routes/contact/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,5 @@
</section>
{/each}
</div>

<p class="text-11 text-center font-mono text-[var(--text-faint)] select-text">v{__APP_VERSION__} ({__GIT_SHA__})</p>
Loading