Skip to content

feat: add generalized map widget (THU-605)#1000

Open
darkbanjo wants to merge 1 commit into
mainfrom
jkab/thu-605-generalize-map-widget
Open

feat: add generalized map widget (THU-605)#1000
darkbanjo wants to merge 1 commit into
mainfrom
jkab/thu-605-generalize-map-widget

Conversation

@darkbanjo

@darkbanjo darkbanjo commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

What

Brings the map widget prototyped on the file-upload demo branch into the product as a first-class, demo-agnostic widget.

  • Adds src/widgets/map: a generic GeoJSON FeatureCollection renderer on MapLibre GL (lazy-loaded), with schema, parser, instructions, stories, and tests.
  • Registers map in the widget registry (src/widgets/index.ts).
  • Adds the maplibre-gl dep.

No demo-specific coupling — the widget renders arbitrary GeoJSON the model emits via <widget:map .../>.

Follow-up

The basemap currently uses the public OpenFreeMap Positron tiles. Swapping to a production-grade tile source is tracked on THU-605 and intentionally out of scope for this extraction.

Test

  • src/widgets/map/geojson.test.ts: 10/10 pass; typecheck clean.

Closes THU-605

🤖 Generated with Claude Code


Note

Low Risk
Self-contained UI widget with validated GeoJSON and safe popup rendering via textContent; no auth or backend changes. Main caveat is reliance on third-party tile URLs and a larger client bundle when maps are shown.

Overview
Adds a map chat widget so agents can render locations via <widget:map data='…' /> with a GeoJSON FeatureCollection (and optional title).

The new src/widgets/map module follows the same pattern as other widgets: Zod schema/parser, AI instructions, MapWidget UI, Storybook stories, and geojson helpers (parse, bounds, labels). The map uses lazy-loaded MapLibre GL, fits the view to features, supports Point/Line/Polygon (and Multi*), simplestyle-spec styling, and click popups for generic label/description only. map is registered in widgetRegistry and maplibre-gl is added as a dependency.

Basemap tiles use the public OpenFreeMap Positron endpoint (noted as a follow-up for production tile sources).

Reviewed by Cursor Bugbot for commit f0041ed. Bugbot is set up for automated code reviews on this repo. Configure here.

Bring the map widget prototyped on the file-upload demo branch into the
product as a first-class, demo-agnostic widget.

- Add src/widgets/map: a generic GeoJSON FeatureCollection renderer on
  MapLibre GL (lazy-loaded), with schema, parser, instructions, and tests.
- Register `map` in the widget registry (src/widgets/index.ts).
- Add maplibre-gl dep.

The widget carries no demo-specific coupling. Note: the basemap uses the
public OpenFreeMap Positron tiles; swapping to a production-grade tile
source is tracked separately on THU-605.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Semgrep Security Scan

No security issues found.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f0041ed. Configure here.

'circle-stroke-color': '#ffffff',
'circle-stroke-width': 2,
},
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi geometries never render

High Severity

Layer filters only match Point, LineString, and Polygon via geometry-type, but validated GeoJSON can include MultiPoint, MultiLineString, and MultiPolygon. MapLibre reports those Multi types separately, so those features never draw despite schema and AI instructions claiming Multi* support.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0041ed. Configure here.

}
// Reset to the skeleton whenever the data changes and we re-init the map.
setReady(false)
let map: MaplibreMap | null = null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map error state never clears

Medium Severity

After setError runs on a failed MapLibre load, a later successful init never resets error. The effect only calls setReady(false) when data changes, so users can see “Couldn’t load the map” under a map that loaded correctly.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0041ed. Configure here.

const popup = new Popup({ closeButton: false, maxWidth: '300px' })
.setLngLat(event.lngLat)
.setDOMContent(node)
.addTo(map)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each click spawns new popup

Low Severity

showPopup creates a new Popup on every feature click without removing the previous one. Clicking several markers can stack multiple popups until the user clicks the bare map.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0041ed. Configure here.

if (!cancelled) {
setReady(true)
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Load handler ignores cancellation

Medium Severity

The map load callback runs addSource and addLayer without checking cancelled, while cleanup sets cancelled and calls map.remove(). If load fires after teardown, it may touch a removed map or call setReady on a stale instance.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f0041ed. Configure here.

@github-actions

Copy link
Copy Markdown

Preview environment deployed 🚀

Service URL
Marketing / blog / docs https://thunderbolt-pr-1000.preview.thunderbolt.io
App https://app-pr-1000.preview.thunderbolt.io
API https://api-pr-1000.preview.thunderbolt.io
Keycloak https://auth-pr-1000.preview.thunderbolt.io
PowerSync https://powersync-pr-1000.preview.thunderbolt.io

Stack: preview-pr-1000 · Commit: f0041ed950ba62d697e0e2435f80b17928da585e

Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — demo@thunderbolt.io / demo by default.

// blank/white map if it initialized before layout settled or while briefly
// hidden (e.g. switching away and back to a chat) and is never told to
// resize. The observer fires on the size change and re-renders at size.
const resizeObserver = new ResizeObserver(() => map?.resize())
@github-actions

Copy link
Copy Markdown

PR Metrics

Metric Value
Lines changed (prod code) +535 / -0
JS bundle size (gzipped) 🟢 682.3 KB → 684.4 KB (+2.1 KB, +0.3%)
Test coverage 🟡 78.09% → 77.89% (-0.2%)
Performance (preview) Preview not ready — Render deploy may have timed out
Accessibility
Best Practices
SEO

Updated Thu, 18 Jun 2026 17:54:43 GMT · run #1947

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant