@sigx/lynx-maps 0.4.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Andreas Ekdahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @sigx/lynx-maps
2
+
3
+ Native map view for sigx-lynx. Backed by:
4
+
5
+ - **iOS** — `MKMapView` (Apple Maps, no key required).
6
+ - **Android** — `com.google.android.gms.maps.MapView` (Google Maps,
7
+ requires an API key — see [setup](#android-api-key-setup) below).
8
+
9
+ Closes [signalxjs/lynx#84](https://github.com/signalxjs/lynx/issues/84).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @sigx/lynx-maps
15
+ pnpm sigx prebuild
16
+ ```
17
+
18
+ The autolinker picks up `signalx-module.json` from this package; prebuild
19
+ regenerates `GeneratedBehaviors.kt` (Android) and
20
+ `GeneratedComponentRegistry.swift` (iOS) so the `<sigx-map>` and
21
+ `<sigx-map-marker>` tags are bound to their native UI classes.
22
+
23
+ ## Quick start
24
+
25
+ ```tsx
26
+ import { Map, MapMarker } from '@sigx/lynx-maps';
27
+
28
+ const region = {
29
+ latitude: 59.3293,
30
+ longitude: 18.0686,
31
+ latitudeDelta: 0.1,
32
+ longitudeDelta: 0.1,
33
+ };
34
+
35
+ export function MapScreen() {
36
+ return (
37
+ <Map
38
+ region={region}
39
+ showsUserLocation
40
+ mapType="standard"
41
+ class="flex-1"
42
+ onRegionChange={(e) => console.log('region', e.detail.region)}
43
+ onMarkerPress={(e) => console.log('marker', e.detail.id)}
44
+ >
45
+ <MapMarker
46
+ coordinate={{ latitude: 59.3293, longitude: 18.0686 }}
47
+ title="Stockholm"
48
+ description="Sweden's capital"
49
+ id="sthlm"
50
+ />
51
+ </Map>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## Props — `<Map>`
57
+
58
+ | Prop | Type | Notes |
59
+ | --- | --- | --- |
60
+ | `region` | `MapRegion` | `{ latitude, longitude, latitudeDelta, longitudeDelta }`. The map snaps to this on every prop change — for "initial region only" semantics, pass it once and keep it in local state. |
61
+ | `showsUserLocation` | `boolean` | Shows the blue location dot. iOS uses `NSLocationWhenInUseUsageDescription` (added automatically); Android requires `ACCESS_FINE_LOCATION` at runtime (declared in the manifest). |
62
+ | `mapType` | `'standard' \| 'satellite' \| 'hybrid'` | Base style. Default `'standard'`. |
63
+ | `onRegionChange` | `(e) => void` | Fires after the user pans/zooms or after a programmatic `region` change. Detail: `{ region }`. |
64
+ | `onPress` | `(e) => void` | User tapped the map (not on a marker). Detail: `{ coordinate }`. |
65
+ | `onMarkerPress` | `(e) => void` | User tapped a marker. Detail: `{ id, coordinate }`. |
66
+ | `class`, `style`, `children` | — | Standard Lynx props. Children should be `<MapMarker>` elements. |
67
+
68
+ ## Props — `<MapMarker>`
69
+
70
+ | Prop | Type | Notes |
71
+ | --- | --- | --- |
72
+ | `coordinate` | `{ latitude, longitude }` | Required. |
73
+ | `title` | `string` | Callout title shown on tap. |
74
+ | `description` | `string` | Callout subtitle. |
75
+ | `id` | `string` | Forwarded as `event.detail.id` on the parent's `onMarkerPress`. |
76
+
77
+ ## Android API key setup
78
+
79
+ Google Maps requires an API key. Get one at
80
+ [console.cloud.google.com](https://console.cloud.google.com/) → APIs &
81
+ Services → Credentials → "Maps SDK for Android".
82
+
83
+ Once you have a key, add the standard meta-data block inside
84
+ `<application>` in `android/app/src/main/AndroidManifest.xml`:
85
+
86
+ ```xml
87
+ <meta-data
88
+ android:name="com.google.android.geo.API_KEY"
89
+ android:value="@string/google_maps_api_key" />
90
+ ```
91
+
92
+ …and define the string in `android/app/src/main/res/values/strings.xml`:
93
+
94
+ ```xml
95
+ <resources>
96
+ <!-- existing entries -->
97
+ <string name="google_maps_api_key" translatable="false">YOUR_KEY_HERE</string>
98
+ </resources>
99
+ ```
100
+
101
+ If you don't add the meta-data block, the map still renders — but with
102
+ Google's "For development purposes only" watermark. That's fine for early
103
+ testing; ship a real key before publishing to the Play Store.
104
+
105
+ On `sigx-lynx-go` (the prebuilt sandbox app) the placeholder watermark is
106
+ expected — we can't bundle a per-user API key in a public binary.
107
+
108
+ ## Limitations (v1)
109
+
110
+ Tracked as v2 follow-ups:
111
+
112
+ - **Imperative methods** (`animateToRegion`, `fitToCoordinates`) need
113
+ Lynx's `UIMethodInvoker` surface, which isn't wired through sigx-lynx
114
+ yet. Same blocker as `WebView.goBack` / `reload`.
115
+ - **Custom marker icons** — markers use the platform default pin.
116
+ - **Polylines, polygons, circles, ground overlays.**
117
+ - **Clustering.**
118
+ - **Offline tiles / Map snapshots.**
119
+ - **Google Maps on iOS** — iOS stays on MapKit (key-free); a future
120
+ follow-up could let apps opt into the Google Maps SDK for iOS.
121
+
122
+ ## Lifecycle notes (Android)
123
+
124
+ `com.google.android.gms.maps.MapView` normally wants Activity lifecycle
125
+ forwarding (`onCreate`/`onResume`/`onPause`/`onDestroy`). v1 calls
126
+ `onCreate` + `onStart` + `onResume` in `createView`, and `onPause` +
127
+ `onStop` in `onDetach`. For a typical screen-level map this works; if the
128
+ host Activity is paused with the map still mounted, tile prefetching
129
+ keeps running until the LynxUI itself detaches. A proper `activityHook`
130
+ plumbing is tracked as a follow-up.
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,19 @@
1
+ package com.sigx.maps
2
+
3
+ import com.lynx.tasm.behavior.Behavior
4
+ import com.lynx.tasm.behavior.LynxContext
5
+ import com.lynx.tasm.behavior.ui.LynxUI
6
+
7
+ /**
8
+ * Registers the `<sigx-map>` JSX tag with Lynx's UI registry.
9
+ *
10
+ * Discovered by the autolinker via `signalx-module.json`'s `android.behaviors`
11
+ * field; the generated `GeneratedBehaviors.attachAll(builder)` calls
12
+ * `builder.addBehavior(SigxMapBehavior())` for every `LynxViewBuilder`
13
+ * in the app (production + dev-client path).
14
+ */
15
+ class SigxMapBehavior : Behavior("sigx-map") {
16
+ override fun createUI(context: LynxContext): LynxUI<*> {
17
+ return SigxMapUI(context)
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ package com.sigx.maps
2
+
3
+ import com.lynx.tasm.behavior.Behavior
4
+ import com.lynx.tasm.behavior.LynxContext
5
+ import com.lynx.tasm.behavior.ui.LynxUI
6
+
7
+ /**
8
+ * Registers the `<sigx-map-marker>` JSX tag with Lynx's UI registry. The
9
+ * actual marker is added to the parent `SigxMapUI`'s GoogleMap on attach;
10
+ * the LynxUI's own view is a zero-size hidden placeholder that never gets
11
+ * laid out, because the parent map intercepts `insertChild` and skips the
12
+ * default add-to-view-hierarchy path.
13
+ */
14
+ class SigxMapMarkerBehavior : Behavior("sigx-map-marker") {
15
+ override fun createUI(context: LynxContext): LynxUI<*> {
16
+ return SigxMapMarkerUI(context)
17
+ }
18
+ }
@@ -0,0 +1,65 @@
1
+ package com.sigx.maps
2
+
3
+ import android.content.Context
4
+ import android.view.View
5
+ import com.google.android.gms.maps.model.Marker
6
+ import com.lynx.tasm.behavior.LynxContext
7
+ import com.lynx.tasm.behavior.LynxProp
8
+ import com.lynx.tasm.behavior.ui.LynxUI
9
+ import org.json.JSONObject
10
+
11
+ /**
12
+ * Native UI for the `<sigx-map-marker>` JSX element on Android.
13
+ *
14
+ * A marker doesn't render its own real view — Google Maps tracks pins
15
+ * separately via [com.google.android.gms.maps.GoogleMap.addMarker]. We
16
+ * still need a `LynxUI<View>` because Lynx's UI tree expects every child
17
+ * to be a UI. The native view is a zero-size hidden `View` that never
18
+ * gets laid out because the parent `SigxMapUI` intercepts `insertChild`
19
+ * for markers and skips the default add-to-view-hierarchy path.
20
+ */
21
+ class SigxMapMarkerUI(context: LynxContext) : LynxUI<View>(context) {
22
+
23
+ internal var owningMap: SigxMapUI? = null
24
+ internal var attachedMarker: Marker? = null
25
+
26
+ internal var coordinateLat: Double = 0.0
27
+ internal var coordinateLng: Double = 0.0
28
+ internal var markerTitle: String? = null
29
+ internal var markerDescription: String? = null
30
+ internal var markerId: String = ""
31
+
32
+ override fun createView(context: Context): View {
33
+ val v = View(context)
34
+ v.visibility = View.GONE
35
+ return v
36
+ }
37
+
38
+ // ── Prop setters ─────────────────────────────────────────────────────
39
+
40
+ @LynxProp(name = "coordinate")
41
+ fun setCoordinate(value: String?) {
42
+ if (value.isNullOrEmpty()) return
43
+ val obj = runCatching { JSONObject(value) }.getOrNull() ?: return
44
+ coordinateLat = obj.optDouble("latitude", coordinateLat)
45
+ coordinateLng = obj.optDouble("longitude", coordinateLng)
46
+ owningMap?.markerDidUpdate(this)
47
+ }
48
+
49
+ @LynxProp(name = "title")
50
+ fun setTitle(value: String?) {
51
+ markerTitle = value
52
+ owningMap?.markerDidUpdate(this)
53
+ }
54
+
55
+ @LynxProp(name = "description")
56
+ fun setDescription(value: String?) {
57
+ markerDescription = value
58
+ owningMap?.markerDidUpdate(this)
59
+ }
60
+
61
+ @LynxProp(name = "marker-id")
62
+ fun setMarkerId(value: String?) {
63
+ markerId = value ?: ""
64
+ }
65
+ }
@@ -0,0 +1,279 @@
1
+ package com.sigx.maps
2
+
3
+ import android.content.Context
4
+ import com.google.android.gms.maps.CameraUpdateFactory
5
+ import com.google.android.gms.maps.GoogleMap
6
+ import com.google.android.gms.maps.MapView
7
+ import com.google.android.gms.maps.model.CameraPosition
8
+ import com.google.android.gms.maps.model.LatLng
9
+ import com.google.android.gms.maps.model.MarkerOptions
10
+ import com.lynx.tasm.behavior.LynxContext
11
+ import com.lynx.tasm.behavior.LynxProp
12
+ import com.lynx.tasm.behavior.ui.LynxBaseUI
13
+ import com.lynx.tasm.behavior.ui.LynxUI
14
+ import com.lynx.tasm.event.LynxDetailEvent
15
+ import org.json.JSONObject
16
+
17
+ /**
18
+ * Native UI for the `<sigx-map>` JSX element on Android.
19
+ *
20
+ * Prop / event surface (v1):
21
+ * - `region` → JSON-stringified `MapRegion`
22
+ * - `shows-user-location` → enable user-location dot (requires permission)
23
+ * - `map-type` → `"standard" | "satellite" | "hybrid"`
24
+ * - `bindregionchange` → CameraIdleListener
25
+ * - `bindpress` → OnMapClickListener
26
+ * - `bindmarkerpress` → OnMarkerClickListener
27
+ *
28
+ * Imperative methods (animateToRegion / fitToCoordinates) are tracked as a
29
+ * v2 follow-up — same UIMethodInvoker blocker as WebView.
30
+ *
31
+ * @remarks
32
+ * Google MapView requires Activity lifecycle forwarding (onCreate /
33
+ * onResume / onPause / onDestroy). The v1 implementation calls onCreate +
34
+ * onStart + onResume in [createView] so the map is interactive
35
+ * immediately, and onPause + onStop + onDestroy in [onDetach]. For an
36
+ * app that backgrounds while a map is visible, this is best-effort —
37
+ * most user-visible maps work, but tile prefetching pauses on
38
+ * backgrounding only after the next prebuild wires a proper
39
+ * `activityHook`. Doc'd in README.
40
+ */
41
+ class SigxMapUI(context: LynxContext) : LynxUI<MapView>(context) {
42
+
43
+ private var googleMap: GoogleMap? = null
44
+ private var pendingRegion: JSONObject? = null
45
+ private var pendingShowUserLocation: Boolean = false
46
+ private var pendingMapType: String? = null
47
+ /** Marker children waiting for GoogleMap to be ready. */
48
+ private val pendingMarkers = mutableListOf<SigxMapMarkerUI>()
49
+ /** All currently-attached markers, by their LynxUI sign. */
50
+ internal val attachedMarkers = mutableMapOf<Int, SigxMapMarkerUI>()
51
+
52
+ override fun createView(context: Context): MapView {
53
+ val map = MapView(context)
54
+ // Bundle=null is the Google-recommended invocation when the host
55
+ // doesn't save/restore MapView state per-instance.
56
+ map.onCreate(null)
57
+ map.onStart()
58
+ map.onResume()
59
+ map.getMapAsync { gmap ->
60
+ googleMap = gmap
61
+ applyPendingState()
62
+ wireListeners(gmap)
63
+ // Attach any markers that arrived before the map was ready.
64
+ for (m in pendingMarkers) attachMarker(m)
65
+ pendingMarkers.clear()
66
+ }
67
+ return map
68
+ }
69
+
70
+ override fun onDetach() {
71
+ super.onDetach()
72
+ // Best-effort lifecycle pairing — see class-level remarks.
73
+ // onDestroy() releases the GL renderer, so call it last to avoid
74
+ // leaking native resources when the host UI tears down.
75
+ runCatching { mView.onPause() }
76
+ runCatching { mView.onStop() }
77
+ runCatching { mView.onDestroy() }
78
+ }
79
+
80
+ // ── Child plumbing ───────────────────────────────────────────────────
81
+
82
+ override fun insertChild(child: LynxBaseUI?, index: Int) {
83
+ if (child is SigxMapMarkerUI) {
84
+ attachMarker(child)
85
+ return
86
+ }
87
+ super.insertChild(child, index)
88
+ }
89
+
90
+ override fun removeChild(child: LynxBaseUI?) {
91
+ if (child is SigxMapMarkerUI) {
92
+ detachMarker(child)
93
+ return
94
+ }
95
+ super.removeChild(child)
96
+ }
97
+
98
+ internal fun attachMarker(marker: SigxMapMarkerUI) {
99
+ marker.owningMap = this
100
+ attachedMarkers[marker.sign] = marker
101
+ val gmap = googleMap
102
+ if (gmap == null) {
103
+ pendingMarkers.add(marker)
104
+ return
105
+ }
106
+ val opts = MarkerOptions()
107
+ .position(LatLng(marker.coordinateLat, marker.coordinateLng))
108
+ .title(marker.markerTitle)
109
+ .snippet(marker.markerDescription)
110
+ val pin = gmap.addMarker(opts)
111
+ if (pin != null) {
112
+ pin.tag = marker.sign
113
+ marker.attachedMarker = pin
114
+ }
115
+ }
116
+
117
+ internal fun detachMarker(marker: SigxMapMarkerUI) {
118
+ attachedMarkers.remove(marker.sign)
119
+ pendingMarkers.remove(marker)
120
+ marker.attachedMarker?.remove()
121
+ marker.attachedMarker = null
122
+ marker.owningMap = null
123
+ }
124
+
125
+ /** Re-sync an already-attached marker's coords/title after a prop change. */
126
+ internal fun markerDidUpdate(marker: SigxMapMarkerUI) {
127
+ val pin = marker.attachedMarker ?: return
128
+ pin.position = LatLng(marker.coordinateLat, marker.coordinateLng)
129
+ pin.title = marker.markerTitle
130
+ pin.snippet = marker.markerDescription
131
+ }
132
+
133
+ // ── State application ────────────────────────────────────────────────
134
+
135
+ private fun applyPendingState() {
136
+ val gmap = googleMap ?: return
137
+ pendingRegion?.let { applyRegion(it) }
138
+ pendingRegion = null
139
+ // SecurityException is thrown by the setter when ACCESS_FINE_LOCATION
140
+ // isn't granted, so the try/catch has to wrap the assignment itself
141
+ // (not the RHS) — see PR #93 review.
142
+ try {
143
+ gmap.isMyLocationEnabled = pendingShowUserLocation
144
+ } catch (_: SecurityException) {
145
+ android.util.Log.w(
146
+ "SigxMap",
147
+ "shows-user-location=true but ACCESS_FINE_LOCATION not granted",
148
+ )
149
+ }
150
+ pendingMapType?.let { applyMapType(it) }
151
+ }
152
+
153
+ private fun applyRegion(obj: JSONObject) {
154
+ val gmap = googleMap ?: return
155
+ val lat = obj.optDouble("latitude", Double.NaN)
156
+ val lon = obj.optDouble("longitude", Double.NaN)
157
+ val lonD = obj.optDouble("longitudeDelta", Double.NaN)
158
+ if (lat.isNaN() || lon.isNaN()) return
159
+ // Map lonDelta to a Google-Maps zoom level. Google's zoom is
160
+ // logarithmic — zoom n shows 360°/2^n degrees of longitude
161
+ // horizontally. Solve for n from the user's lonDelta: invert and
162
+ // log. Clamp to Google's [2, 21] zoom range. latitudeDelta is
163
+ // ignored because the visible viewport's aspect ratio is fixed by
164
+ // the view size — fitting both deltas would require knowing the
165
+ // pixel dimensions, which is deferred to v2's `fitToCoordinates`.
166
+ val zoom = if (lonD > 0.0) {
167
+ val z = Math.log(360.0 / lonD) / Math.log(2.0)
168
+ z.toFloat().coerceIn(2f, 21f)
169
+ } else {
170
+ 12f
171
+ }
172
+ gmap.moveCamera(
173
+ CameraUpdateFactory.newCameraPosition(
174
+ CameraPosition.Builder().target(LatLng(lat, lon)).zoom(zoom).build(),
175
+ ),
176
+ )
177
+ }
178
+
179
+ private fun applyMapType(value: String) {
180
+ val gmap = googleMap ?: return
181
+ gmap.mapType = when (value) {
182
+ "satellite" -> GoogleMap.MAP_TYPE_SATELLITE
183
+ "hybrid" -> GoogleMap.MAP_TYPE_HYBRID
184
+ else -> GoogleMap.MAP_TYPE_NORMAL
185
+ }
186
+ }
187
+
188
+ private fun wireListeners(gmap: GoogleMap) {
189
+ gmap.setOnCameraIdleListener {
190
+ val target = gmap.cameraPosition.target
191
+ // Approximate the LynxRegion shape — Google reports zoom rather
192
+ // than deltas, so back-compute span from the visible bounds.
193
+ val bounds = gmap.projection.visibleRegion.latLngBounds
194
+ val latD = bounds.northeast.latitude - bounds.southwest.latitude
195
+ val lonD = bounds.northeast.longitude - bounds.southwest.longitude
196
+ fireEvent(
197
+ "regionchange",
198
+ mapOf(
199
+ "region" to mapOf(
200
+ "latitude" to target.latitude,
201
+ "longitude" to target.longitude,
202
+ "latitudeDelta" to latD,
203
+ "longitudeDelta" to lonD,
204
+ ),
205
+ ),
206
+ )
207
+ }
208
+ gmap.setOnMapClickListener { latLng ->
209
+ fireEvent(
210
+ "press",
211
+ mapOf(
212
+ "coordinate" to mapOf(
213
+ "latitude" to latLng.latitude,
214
+ "longitude" to latLng.longitude,
215
+ ),
216
+ ),
217
+ )
218
+ }
219
+ gmap.setOnMarkerClickListener { gMarker ->
220
+ val sign = gMarker.tag as? Int ?: return@setOnMarkerClickListener false
221
+ val marker = attachedMarkers[sign] ?: return@setOnMarkerClickListener false
222
+ fireEvent(
223
+ "markerpress",
224
+ mapOf(
225
+ "id" to marker.markerId,
226
+ "coordinate" to mapOf(
227
+ "latitude" to marker.coordinateLat,
228
+ "longitude" to marker.coordinateLng,
229
+ ),
230
+ ),
231
+ )
232
+ // Return false so Google's default behaviour (callout + camera
233
+ // re-centre) still runs.
234
+ false
235
+ }
236
+ }
237
+
238
+ // ── Prop setters ─────────────────────────────────────────────────────
239
+
240
+ @LynxProp(name = "region")
241
+ fun setRegion(value: String?) {
242
+ if (value.isNullOrEmpty()) return
243
+ val obj = runCatching { JSONObject(value) }.getOrNull() ?: return
244
+ if (googleMap == null) {
245
+ pendingRegion = obj
246
+ } else {
247
+ applyRegion(obj)
248
+ }
249
+ }
250
+
251
+ @LynxProp(name = "shows-user-location")
252
+ fun setShowsUserLocation(value: Boolean) {
253
+ pendingShowUserLocation = value
254
+ val gmap = googleMap ?: return
255
+ try {
256
+ gmap.isMyLocationEnabled = value
257
+ } catch (_: SecurityException) {
258
+ android.util.Log.w(
259
+ "SigxMap",
260
+ "shows-user-location=true but ACCESS_FINE_LOCATION not granted",
261
+ )
262
+ }
263
+ }
264
+
265
+ @LynxProp(name = "map-type")
266
+ fun setMapType(value: String?) {
267
+ val v = value ?: "standard"
268
+ pendingMapType = v
269
+ applyMapType(v)
270
+ }
271
+
272
+ // ── Event firing ─────────────────────────────────────────────────────
273
+
274
+ private fun fireEvent(name: String, params: Map<String, Any?>) {
275
+ val event = LynxDetailEvent(sign, name)
276
+ for ((k, v) in params) event.addDetail(k, v)
277
+ lynxContext.eventEmitter.sendCustomEvent(event)
278
+ }
279
+ }
package/dist/Map.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { type Define } from '@sigx/lynx';
2
+ import './jsx-augment.js';
3
+ import type { MapRegion, MapType, MapPressEvent, MapRegionChangeEvent, MapMarkerPressEvent } from './jsx-augment.js';
4
+ export type MapProps = Define.Prop<'region', MapRegion, false> & Define.Prop<'showsUserLocation', boolean, false> & Define.Prop<'mapType', MapType, false> & Define.Prop<'class', string, false> & Define.Prop<'style', string | Record<string, string | number>, false> & Define.Prop<'onRegionChange', (e: MapRegionChangeEvent) => void, false> & Define.Prop<'onPress', (e: MapPressEvent) => void, false> & Define.Prop<'onMarkerPress', (e: MapMarkerPressEvent) => void, false> & Define.Prop<'children', unknown, false>;
5
+ /**
6
+ * Native map view.
7
+ *
8
+ * Backed by `MKMapView` on iOS and `com.google.android.gms.maps.MapView`
9
+ * on Android. Pass any number of `<MapMarker>` children to render pins.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <Map
14
+ * region={{
15
+ * latitude: 59.3293,
16
+ * longitude: 18.0686,
17
+ * latitudeDelta: 0.1,
18
+ * longitudeDelta: 0.1,
19
+ * }}
20
+ * showsUserLocation
21
+ * onRegionChange={(e) => console.log('region', e.detail.region)}
22
+ * onMarkerPress={(e) => console.log('marker', e.detail.id)}
23
+ * >
24
+ * <MapMarker
25
+ * coordinate={{ latitude: 59.3293, longitude: 18.0686 }}
26
+ * title="Stockholm"
27
+ * />
28
+ * </Map>
29
+ * ```
30
+ *
31
+ * @remarks
32
+ * - **Android API key**: Google Maps requires an API key. See this
33
+ * package's README for setup. The placeholder used by `sigx-lynx-go`
34
+ * shows the "For development purposes only" watermark — that's
35
+ * expected until you provide your own key.
36
+ * - **Imperative methods** (`animateToRegion`, `fitToCoordinates`) are
37
+ * tracked as a v2 follow-up — they need the Lynx UIMethodInvoker
38
+ * surface which isn't wired through sigx-lynx yet. The same gap
39
+ * blocks WebView's `goBack` / `reload`.
40
+ */
41
+ export declare const Map: import("@sigx/runtime-core").ComponentFactory<MapProps, void, {}>;
package/dist/Map.js ADDED
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
+ import { component } from '@sigx/lynx';
3
+ import './jsx-augment.js';
4
+ /**
5
+ * Native map view.
6
+ *
7
+ * Backed by `MKMapView` on iOS and `com.google.android.gms.maps.MapView`
8
+ * on Android. Pass any number of `<MapMarker>` children to render pins.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <Map
13
+ * region={{
14
+ * latitude: 59.3293,
15
+ * longitude: 18.0686,
16
+ * latitudeDelta: 0.1,
17
+ * longitudeDelta: 0.1,
18
+ * }}
19
+ * showsUserLocation
20
+ * onRegionChange={(e) => console.log('region', e.detail.region)}
21
+ * onMarkerPress={(e) => console.log('marker', e.detail.id)}
22
+ * >
23
+ * <MapMarker
24
+ * coordinate={{ latitude: 59.3293, longitude: 18.0686 }}
25
+ * title="Stockholm"
26
+ * />
27
+ * </Map>
28
+ * ```
29
+ *
30
+ * @remarks
31
+ * - **Android API key**: Google Maps requires an API key. See this
32
+ * package's README for setup. The placeholder used by `sigx-lynx-go`
33
+ * shows the "For development purposes only" watermark — that's
34
+ * expected until you provide your own key.
35
+ * - **Imperative methods** (`animateToRegion`, `fitToCoordinates`) are
36
+ * tracked as a v2 follow-up — they need the Lynx UIMethodInvoker
37
+ * surface which isn't wired through sigx-lynx yet. The same gap
38
+ * blocks WebView's `goBack` / `reload`.
39
+ */
40
+ export const Map = component(({ props }) => {
41
+ return () => (_jsx("sigx-map", { region: props.region == null ? undefined : JSON.stringify(props.region), "shows-user-location": props.showsUserLocation, "map-type": props.mapType, class: props.class, style: props.style, bindregionchange: props.onRegionChange, bindpress: props.onPress, bindmarkerpress: props.onMarkerPress, children: props.children }));
42
+ });
@@ -0,0 +1,29 @@
1
+ import { type Define } from '@sigx/lynx';
2
+ import './jsx-augment.js';
3
+ import type { MapCoordinate } from './jsx-augment.js';
4
+ export type MapMarkerProps = Define.Prop<'coordinate', MapCoordinate, true> & Define.Prop<'title', string, false> & Define.Prop<'description', string, false> & Define.Prop<'id', string, false>;
5
+ /**
6
+ * Marker pin on a `<Map>`.
7
+ *
8
+ * Coordinates are required; `title` / `description` show in the callout
9
+ * when the user taps the marker. `id` is surfaced as
10
+ * `event.detail.id` in the parent map's `onMarkerPress` handler — set
11
+ * one if multiple markers share the same coordinate.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Map>
16
+ * <MapMarker
17
+ * coordinate={{ latitude: 59.3293, longitude: 18.0686 }}
18
+ * title="Stockholm"
19
+ * description="Sweden's capital"
20
+ * id="sthlm"
21
+ * />
22
+ * </Map>
23
+ * ```
24
+ *
25
+ * @remarks
26
+ * Marker pins are intentionally minimal in v1 — no custom icons, no
27
+ * draggable pins, no callout views. Those land in v2.
28
+ */
29
+ export declare const MapMarker: import("@sigx/runtime-core").ComponentFactory<MapMarkerProps, void, {}>;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
+ import { component } from '@sigx/lynx';
3
+ import './jsx-augment.js';
4
+ /**
5
+ * Marker pin on a `<Map>`.
6
+ *
7
+ * Coordinates are required; `title` / `description` show in the callout
8
+ * when the user taps the marker. `id` is surfaced as
9
+ * `event.detail.id` in the parent map's `onMarkerPress` handler — set
10
+ * one if multiple markers share the same coordinate.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <Map>
15
+ * <MapMarker
16
+ * coordinate={{ latitude: 59.3293, longitude: 18.0686 }}
17
+ * title="Stockholm"
18
+ * description="Sweden's capital"
19
+ * id="sthlm"
20
+ * />
21
+ * </Map>
22
+ * ```
23
+ *
24
+ * @remarks
25
+ * Marker pins are intentionally minimal in v1 — no custom icons, no
26
+ * draggable pins, no callout views. Those land in v2.
27
+ */
28
+ export const MapMarker = component(({ props }) => {
29
+ return () => (_jsx("sigx-map-marker", { coordinate: JSON.stringify(props.coordinate), title: props.title, description: props.description, "marker-id": props.id }));
30
+ });
@@ -0,0 +1,6 @@
1
+ import './jsx-augment.js';
2
+ export { Map } from './Map.js';
3
+ export type { MapProps } from './Map.js';
4
+ export { MapMarker } from './MapMarker.js';
5
+ export type { MapMarkerProps } from './MapMarker.js';
6
+ export type { MapRegion, MapCoordinate, MapType, MapRegionChangeEvent, MapRegionChangeEventDetail, MapPressEvent, MapPressEventDetail, MapMarkerPressEvent, MapMarkerPressEventDetail, SigxMapAttributes, SigxMapMarkerAttributes, } from './jsx-augment.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import './jsx-augment.js';
2
+ export { Map } from './Map.js';
3
+ export { MapMarker } from './MapMarker.js';
@@ -0,0 +1,109 @@
1
+ /**
2
+ * JSX intrinsic type augmentation for `<sigx-map>` and `<sigx-map-marker>`.
3
+ *
4
+ * Importing this module registers both tags on `JSX.IntrinsicElements` with
5
+ * the prop + event surface implemented by `SigxMapUI` / `SigxMapMarkerUI`
6
+ * (iOS) and the same-named Kotlin classes (Android). Pulled in automatically
7
+ * by `@sigx/lynx-maps`'s entry point so consumers do not need to import it
8
+ * directly.
9
+ *
10
+ * Element availability requires `sigx prebuild` to have run after adding
11
+ * this package as a dependency — the autolinker emits the `LynxConfig`
12
+ * registration (iOS) and `Behavior` attachment (Android) that bind the tags
13
+ * to the native UI classes.
14
+ */
15
+ import type { LynxCommonAttributes, LynxEventHandler } from '@sigx/lynx-runtime';
16
+ /**
17
+ * A rectangular region of the map. `latitudeDelta` / `longitudeDelta` define
18
+ * the span (in degrees) visible above/below and left/right of `latitude` /
19
+ * `longitude`. Matches the react-native-maps `Region` shape so docs and
20
+ * coordinate utilities translate cleanly.
21
+ */
22
+ export interface MapRegion {
23
+ latitude: number;
24
+ longitude: number;
25
+ latitudeDelta: number;
26
+ longitudeDelta: number;
27
+ }
28
+ export interface MapCoordinate {
29
+ latitude: number;
30
+ longitude: number;
31
+ }
32
+ export type MapType = 'standard' | 'satellite' | 'hybrid';
33
+ export interface MapRegionChangeEventDetail {
34
+ region: MapRegion;
35
+ [k: string]: unknown;
36
+ }
37
+ export interface MapRegionChangeEvent {
38
+ type: 'regionchange';
39
+ detail: MapRegionChangeEventDetail;
40
+ }
41
+ export interface MapPressEventDetail {
42
+ coordinate: MapCoordinate;
43
+ [k: string]: unknown;
44
+ }
45
+ export interface MapPressEvent {
46
+ type: 'press';
47
+ detail: MapPressEventDetail;
48
+ }
49
+ export interface MapMarkerPressEventDetail {
50
+ /** Marker's `marker-id` prop, or empty string if none was set. */
51
+ id: string;
52
+ coordinate: MapCoordinate;
53
+ [k: string]: unknown;
54
+ }
55
+ export interface MapMarkerPressEvent {
56
+ type: 'markerpress';
57
+ detail: MapMarkerPressEventDetail;
58
+ }
59
+ export interface SigxMapAttributes extends LynxCommonAttributes {
60
+ /**
61
+ * Visible region. The map snaps to this on every prop change, so
62
+ * authors who only want to set an *initial* region should pass it once
63
+ * and avoid re-passing it on every render (use local state).
64
+ *
65
+ * Serialised as a JSON string on the wire — the native side parses it
66
+ * back into a region struct. Lynx props don't support object types
67
+ * directly today, so JSON-on-the-wire is the standard sigx-lynx idiom.
68
+ */
69
+ region?: string;
70
+ /** Show the user's current location dot. Requires location permission. */
71
+ 'shows-user-location'?: boolean;
72
+ /** Base map style. */
73
+ 'map-type'?: MapType;
74
+ /** Visible region changed (either programmatic or user pan/zoom). */
75
+ bindregionchange?: LynxEventHandler<MapRegionChangeEvent>;
76
+ /** User tapped the map (not on a marker). */
77
+ bindpress?: LynxEventHandler<MapPressEvent>;
78
+ /** User tapped a marker. */
79
+ bindmarkerpress?: LynxEventHandler<MapMarkerPressEvent>;
80
+ children?: unknown;
81
+ }
82
+ export interface SigxMapMarkerAttributes extends LynxCommonAttributes {
83
+ /**
84
+ * Marker location. JSON-stringified `{ "latitude": …, "longitude": … }`
85
+ * for the same reason as `region` on `<sigx-map>` — Lynx props don't
86
+ * support nested objects directly. Required: a marker without a
87
+ * coordinate has nowhere to render.
88
+ */
89
+ coordinate: string;
90
+ /** Callout title shown on tap. */
91
+ title?: string;
92
+ /** Callout subtitle shown below the title. */
93
+ description?: string;
94
+ /**
95
+ * Stable identifier surfaced as `event.detail.id` in `bindmarkerpress`.
96
+ * Useful when many markers share the same coordinate (e.g. clustered
97
+ * entries) and the press handler needs to disambiguate.
98
+ */
99
+ 'marker-id'?: string;
100
+ }
101
+ declare global {
102
+ namespace JSX {
103
+ interface IntrinsicElements {
104
+ 'sigx-map': SigxMapAttributes;
105
+ 'sigx-map-marker': SigxMapMarkerAttributes;
106
+ }
107
+ }
108
+ }
109
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ import Foundation
2
+ import UIKit
3
+ import MapKit
4
+ import Lynx
5
+
6
+ /// Native UI for the `<sigx-map-marker>` JSX element.
7
+ ///
8
+ /// A marker doesn't render its own real view — Google Maps and MapKit both
9
+ /// model pins as data attached to the map. We still need a `LynxUI<UIView>`
10
+ /// because Lynx's UI tree expects every child to be a UI. The native view is
11
+ /// a zero-size hidden `UIView` that never gets laid out: the parent
12
+ /// `SigxMapUI` intercepts `insertChild` for markers and skips calling super,
13
+ /// so the dummy view is created but never attached to the map view hierarchy.
14
+ ///
15
+ /// Prop surface (v1):
16
+ /// - `coordinate` → JSON-stringified `{ latitude, longitude }`
17
+ /// - `title` → callout title
18
+ /// - `description` → callout subtitle
19
+ /// - `marker-id` → forwarded as `event.detail.id` on `bindmarkerpress`
20
+ // Class is NOT marked `@objc` — Swift forbids that on generic subclasses
21
+ // of an ObjC lightweight-generic type like `LynxUI<__covariant V>`. Member-
22
+ // level `@objc` / `@objc(name)` annotations still bridge because `LynxUI`
23
+ // itself is `@objc`, so `__lynx_prop_config__*` discovery still works.
24
+ public class SigxMapMarkerUI: LynxUI<UIView> {
25
+ internal weak var owningMap: SigxMapUI?
26
+
27
+ internal var currentCoordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
28
+ internal var currentTitle: String?
29
+ internal var currentSubtitle: String?
30
+ internal var markerId: String = ""
31
+
32
+ public override func createView() -> UIView? {
33
+ let v = UIView(frame: .zero)
34
+ v.isHidden = true
35
+ return v
36
+ }
37
+
38
+ // Explicit prop-setter registration. See SigxWebViewUI for rationale.
39
+ @objc public class func propSetterLookUp() -> NSArray {
40
+ return [
41
+ ["coordinate", "setCoordinate:requestReset:"],
42
+ ["title", "setTitle:requestReset:"],
43
+ ["description", "setDescription:requestReset:"],
44
+ ["marker-id", "setMarkerId:requestReset:"],
45
+ ] as NSArray
46
+ }
47
+
48
+ internal func makeAnnotation() -> MKPointAnnotation {
49
+ let a = MKPointAnnotation()
50
+ a.coordinate = currentCoordinate
51
+ a.title = currentTitle
52
+ a.subtitle = currentSubtitle
53
+ return a
54
+ }
55
+
56
+ // MARK: - Prop setters
57
+
58
+ @objc public func setCoordinate(_ value: NSString?, requestReset: Bool) {
59
+ guard let raw = value as String?, !raw.isEmpty,
60
+ let data = raw.data(using: .utf8),
61
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
62
+ let lat = (obj["latitude"] as? NSNumber)?.doubleValue,
63
+ let lon = (obj["longitude"] as? NSNumber)?.doubleValue
64
+ else { return }
65
+ currentCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
66
+ owningMap?.markerDidUpdate(self)
67
+ }
68
+
69
+ @objc(__lynx_prop_config__coordinate)
70
+ public class func __lynxPropConfigCoordinate() -> [String] {
71
+ return ["coordinate", "setCoordinate", "NSString *"]
72
+ }
73
+
74
+ @objc public func setTitle(_ value: NSString?, requestReset: Bool) {
75
+ currentTitle = value as String?
76
+ owningMap?.markerDidUpdate(self)
77
+ }
78
+
79
+ @objc(__lynx_prop_config__title)
80
+ public class func __lynxPropConfigTitle() -> [String] {
81
+ return ["title", "setTitle", "NSString *"]
82
+ }
83
+
84
+ @objc public func setDescription(_ value: NSString?, requestReset: Bool) {
85
+ currentSubtitle = value as String?
86
+ owningMap?.markerDidUpdate(self)
87
+ }
88
+
89
+ @objc(__lynx_prop_config__description)
90
+ public class func __lynxPropConfigDescription() -> [String] {
91
+ return ["description", "setDescription", "NSString *"]
92
+ }
93
+
94
+ @objc public func setMarkerId(_ value: NSString?, requestReset: Bool) {
95
+ markerId = (value as String?) ?? ""
96
+ }
97
+
98
+ @objc(__lynx_prop_config__marker_id)
99
+ public class func __lynxPropConfigMarkerId() -> [String] {
100
+ return ["marker-id", "setMarkerId", "NSString *"]
101
+ }
102
+ }
@@ -0,0 +1,236 @@
1
+ import Foundation
2
+ import UIKit
3
+ import MapKit
4
+ import Lynx
5
+
6
+ /// Native UI for the `<sigx-map>` JSX element.
7
+ ///
8
+ /// Registered via the autolinker — `signalx-module.json`'s `ios.uiComponents`
9
+ /// produces a `config.registerUI(SigxMapUI.self, withName: "sigx-map")` call
10
+ /// in the generated `GeneratedComponentRegistry.swift`.
11
+ ///
12
+ /// Prop / event surface (v1):
13
+ /// - `region` → JSON-stringified `MapRegion`
14
+ /// - `shows-user-location` → toggle the user-location dot
15
+ /// - `map-type` → `"standard" | "satellite" | "hybrid"`
16
+ /// - `bindregionchange` → region changed (programmatic or user gesture)
17
+ /// - `bindpress` → user tapped the map (not a marker)
18
+ /// - `bindmarkerpress` → user tapped a marker
19
+ ///
20
+ /// Imperative methods (animateToRegion / fitToCoordinates) are tracked as a
21
+ /// v2 follow-up — they need the Lynx UIMethodInvoker surface which isn't
22
+ /// wired through sigx-lynx yet (same blocker as WebView.goBack / reload).
23
+ // Class is NOT marked `@objc` — Swift forbids that on generic subclasses
24
+ // of an ObjC lightweight-generic type like `LynxUI<__covariant V>`. Member-
25
+ // level `@objc` / `@objc(name)` annotations still bridge because `LynxUI`
26
+ // itself is `@objc`, so `__lynx_prop_config__*` and
27
+ // `__lynx_ui_method_config__*` discovery still works.
28
+ public class SigxMapUI: LynxUI<MKMapView> {
29
+
30
+ private lazy var mapDelegate = SigxMapDelegate(owner: self)
31
+ private lazy var tapGesture: UITapGestureRecognizer = {
32
+ let gr = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
33
+ gr.cancelsTouchesInView = false
34
+ return gr
35
+ }()
36
+ /// Tracks the `MKPointAnnotation` per marker child so we can rebuild
37
+ /// callout titles / coordinates in place rather than re-adding pins on
38
+ /// every prop change.
39
+ private var markerAnnotations: [ObjectIdentifier: MKPointAnnotation] = [:]
40
+ /// Reverse index: annotation identity → marker UI, for translating
41
+ /// `didSelect` callbacks back into `bindmarkerpress` events.
42
+ private var markersByAnnotation: [ObjectIdentifier: SigxMapMarkerUI] = [:]
43
+
44
+ // MARK: - LynxUI overrides
45
+
46
+ public override func createView() -> MKMapView? {
47
+ let map = MKMapView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
48
+ map.translatesAutoresizingMaskIntoConstraints = true
49
+ map.autoresizingMask = [.flexibleWidth, .flexibleHeight]
50
+ map.delegate = mapDelegate
51
+ map.addGestureRecognizer(tapGesture)
52
+ return map
53
+ }
54
+
55
+ // Explicit prop-setter registration. See SigxWebViewUI for rationale.
56
+ @objc public class func propSetterLookUp() -> NSArray {
57
+ return [
58
+ ["region", "setRegion:requestReset:"],
59
+ ["shows-user-location", "setShowsUserLocation:requestReset:"],
60
+ ["map-type", "setMapType:requestReset:"],
61
+ ] as NSArray
62
+ }
63
+
64
+ // Lynx's child-management hooks live on `LynxComponent<D>` where for
65
+ // `LynxUI` the generic `D` is `LynxUI*`. ObjC lightweight generics
66
+ // import into Swift as the upper bound, so the override signature uses
67
+ // `LynxUI<UIView>` (the declared bound `__covariant V : UIView*`).
68
+ // Both insert and remove take `atIndex` per the LynxComponent.h ABI.
69
+ // Swift imports the ObjC `insertChild:atIndex:` selector as
70
+ // `insertChild(_:at:)` and the same for removeChild. Override matches.
71
+ public override func insertChild(_ child: LynxUI<UIView>!, at index: Int) {
72
+ // Map markers participate in the Lynx UI tree as children of the map,
73
+ // but we don't want their UIViews added to MKMapView — markers
74
+ // render as `MKAnnotation`s on the native side. Intercept marker
75
+ // children, register the annotation, and skip super.
76
+ if let marker = child as? SigxMapMarkerUI {
77
+ attachMarker(marker)
78
+ return
79
+ }
80
+ super.insertChild(child, at: index)
81
+ }
82
+
83
+ public override func removeChild(_ child: LynxUI<UIView>!, at index: Int) {
84
+ if let marker = child as? SigxMapMarkerUI {
85
+ detachMarker(marker)
86
+ return
87
+ }
88
+ super.removeChild(child, at: index)
89
+ }
90
+
91
+ // MARK: - Marker plumbing
92
+
93
+ func attachMarker(_ marker: SigxMapMarkerUI) {
94
+ marker.owningMap = self
95
+ let annotation = marker.makeAnnotation()
96
+ markerAnnotations[ObjectIdentifier(marker)] = annotation
97
+ markersByAnnotation[ObjectIdentifier(annotation)] = marker
98
+ view().addAnnotation(annotation)
99
+ }
100
+
101
+ func detachMarker(_ marker: SigxMapMarkerUI) {
102
+ if let annotation = markerAnnotations.removeValue(forKey: ObjectIdentifier(marker)) {
103
+ markersByAnnotation.removeValue(forKey: ObjectIdentifier(annotation))
104
+ view().removeAnnotation(annotation)
105
+ }
106
+ marker.owningMap = nil
107
+ }
108
+
109
+ /// Called by `SigxMapMarkerUI` when its coordinate / title / description
110
+ /// props change so the on-screen annotation reflects the new values
111
+ /// without re-adding the pin (which would clear selection state).
112
+ func markerDidUpdate(_ marker: SigxMapMarkerUI) {
113
+ guard let annotation = markerAnnotations[ObjectIdentifier(marker)] else { return }
114
+ annotation.coordinate = marker.currentCoordinate
115
+ annotation.title = marker.currentTitle
116
+ annotation.subtitle = marker.currentSubtitle
117
+ }
118
+
119
+ // MARK: - Prop setters
120
+
121
+ @objc public func setRegion(_ value: NSString?, requestReset: Bool) {
122
+ guard let raw = value as String?, !raw.isEmpty else { return }
123
+ guard
124
+ let data = raw.data(using: .utf8),
125
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
126
+ let lat = (obj["latitude"] as? NSNumber)?.doubleValue,
127
+ let lon = (obj["longitude"] as? NSNumber)?.doubleValue,
128
+ let latD = (obj["latitudeDelta"] as? NSNumber)?.doubleValue,
129
+ let lonD = (obj["longitudeDelta"] as? NSNumber)?.doubleValue
130
+ else {
131
+ NSLog("[SigxMap] Ignoring malformed region prop: \(raw.prefix(64))")
132
+ return
133
+ }
134
+ let region = MKCoordinateRegion(
135
+ center: CLLocationCoordinate2D(latitude: lat, longitude: lon),
136
+ span: MKCoordinateSpan(latitudeDelta: latD, longitudeDelta: lonD)
137
+ )
138
+ view().setRegion(region, animated: false)
139
+ }
140
+
141
+ @objc(__lynx_prop_config__region)
142
+ public class func __lynxPropConfigRegion() -> [String] {
143
+ return ["region", "setRegion", "NSString *"]
144
+ }
145
+
146
+ @objc public func setShowsUserLocation(_ value: Bool, requestReset: Bool) {
147
+ view().showsUserLocation = value
148
+ }
149
+
150
+ @objc(__lynx_prop_config__shows_user_location)
151
+ public class func __lynxPropConfigShowsUserLocation() -> [String] {
152
+ return ["shows-user-location", "setShowsUserLocation", "BOOL"]
153
+ }
154
+
155
+ @objc public func setMapType(_ value: NSString?, requestReset: Bool) {
156
+ switch (value as String?) ?? "" {
157
+ case "satellite": view().mapType = .satellite
158
+ case "hybrid": view().mapType = .hybrid
159
+ default: view().mapType = .standard
160
+ }
161
+ }
162
+
163
+ @objc(__lynx_prop_config__map_type)
164
+ public class func __lynxPropConfigMapType() -> [String] {
165
+ return ["map-type", "setMapType", "NSString *"]
166
+ }
167
+
168
+ // MARK: - Gesture handling
169
+
170
+ @objc private func handleTap(_ gr: UITapGestureRecognizer) {
171
+ let map = view()
172
+ let point = gr.location(in: map)
173
+ // Skip taps on the existing annotations — those are surfaced via
174
+ // didSelect in the delegate as `bindmarkerpress`.
175
+ if map.hitTest(point, with: nil) is MKAnnotationView { return }
176
+ let coord = map.convert(point, toCoordinateFrom: map)
177
+ fireEvent("press", params: [
178
+ "coordinate": [
179
+ "latitude": coord.latitude,
180
+ "longitude": coord.longitude,
181
+ ],
182
+ ])
183
+ }
184
+
185
+ // MARK: - Event firing
186
+
187
+ internal func fireEvent(_ name: String, params: [String: Any]) {
188
+ let event = LynxCustomEvent(name: name, targetSign: sign, params: params)
189
+ // Swift renames the ObjC `sendCustomEvent:` selector to `send(_:)`
190
+ // when LynxCustomEvent is the parameter type — same underlying method.
191
+ context?.eventEmitter?.send(event)
192
+ }
193
+
194
+ internal func emitRegionChange(_ region: MKCoordinateRegion) {
195
+ fireEvent("regionchange", params: [
196
+ "region": [
197
+ "latitude": region.center.latitude,
198
+ "longitude": region.center.longitude,
199
+ "latitudeDelta": region.span.latitudeDelta,
200
+ "longitudeDelta": region.span.longitudeDelta,
201
+ ],
202
+ ])
203
+ }
204
+
205
+ internal func emitMarkerPress(_ marker: SigxMapMarkerUI) {
206
+ fireEvent("markerpress", params: [
207
+ "id": marker.markerId,
208
+ "coordinate": [
209
+ "latitude": marker.currentCoordinate.latitude,
210
+ "longitude": marker.currentCoordinate.longitude,
211
+ ],
212
+ ])
213
+ }
214
+
215
+ internal func marker(for annotation: MKAnnotation) -> SigxMapMarkerUI? {
216
+ guard let obj = annotation as? AnyObject else { return nil }
217
+ return markersByAnnotation[ObjectIdentifier(obj)]
218
+ }
219
+ }
220
+
221
+ /// `MKMapViewDelegate` forwarder — keeps the public class slim and avoids
222
+ /// the `NSObject` Obj-C runtime drag on `SigxMapUI` itself.
223
+ final class SigxMapDelegate: NSObject, MKMapViewDelegate {
224
+ private weak var owner: SigxMapUI?
225
+
226
+ init(owner: SigxMapUI) { self.owner = owner }
227
+
228
+ func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
229
+ owner?.emitRegionChange(mapView.region)
230
+ }
231
+
232
+ func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
233
+ guard let annotation = view.annotation, let marker = owner?.marker(for: annotation) else { return }
234
+ owner?.emitMarkerPress(marker)
235
+ }
236
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@sigx/lynx-maps",
3
+ "version": "0.4.1",
4
+ "description": "Native map view for sigx-lynx (MKMapView on iOS, Google Maps on Android).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./signalx-module.json": "./signalx-module.json",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "ios",
20
+ "android",
21
+ "signalx-module.json",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "peerDependencies": {
26
+ "@sigx/lynx": "^0.4.1"
27
+ },
28
+ "devDependencies": {
29
+ "@typescript/native-preview": "7.0.0-dev.20260521.1",
30
+ "typescript": "^6.0.3",
31
+ "vitest": "^4.1.7",
32
+ "@sigx/lynx": "^0.4.1"
33
+ },
34
+ "author": "Andreas Ekdahl",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/signalxjs/lynx.git",
39
+ "directory": "packages/lynx-maps"
40
+ },
41
+ "homepage": "https://github.com/signalxjs/lynx/tree/main/packages/lynx-maps",
42
+ "bugs": {
43
+ "url": "https://github.com/signalxjs/lynx/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "keywords": [
49
+ "signalx",
50
+ "sigx",
51
+ "lynx",
52
+ "maps",
53
+ "mkmapview",
54
+ "google-maps",
55
+ "ios",
56
+ "android"
57
+ ],
58
+ "scripts": {
59
+ "build": "node ../../scripts/clean.mjs dist && tsgo",
60
+ "dev": "tsgo --watch",
61
+ "test": "vitest run",
62
+ "clean": "node ../../scripts/clean.mjs dist .turbo"
63
+ }
64
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "Maps",
3
+ "package": "@sigx/lynx-maps",
4
+ "description": "Native map view (MKMapView / Google Maps)",
5
+ "platforms": ["android", "ios"],
6
+ "ios": {
7
+ "sourceDir": "ios",
8
+ "uiComponents": [
9
+ { "name": "sigx-map", "uiClass": "SigxMapUI" },
10
+ { "name": "sigx-map-marker", "uiClass": "SigxMapMarkerUI" }
11
+ ],
12
+ "usageDescriptions": {
13
+ "NSLocationWhenInUseUsageDescription": "Show your location on the map."
14
+ }
15
+ },
16
+ "android": {
17
+ "sourceDir": "android",
18
+ "behaviors": [
19
+ { "name": "sigx-map", "behaviorClass": "com.sigx.maps.SigxMapBehavior" },
20
+ { "name": "sigx-map-marker", "behaviorClass": "com.sigx.maps.SigxMapMarkerBehavior" }
21
+ ],
22
+ "permissions": [
23
+ "android.permission.ACCESS_FINE_LOCATION",
24
+ "android.permission.ACCESS_COARSE_LOCATION"
25
+ ],
26
+ "dependencies": [
27
+ "com.google.android.gms:play-services-maps:19.0.0"
28
+ ]
29
+ }
30
+ }