All notable changes to dash-leaflet2 are documented in this file.
The format follows Keep a Changelog and this
project adheres to Semantic Versioning. Because
the project tracks leaflet@2.0.0-alpha.1, the 0.x line is itself alpha — APIs
will move until v2 leaves alpha upstream.
Nothing yet.
The rest of the 2plot network standard, from the checklist's "found on the
email pass" — the items that each bit a satellite which already looked
finished. Documentation site and network wiring only; no dl2.* component
changed.
Deploy note.
og:imagenow declares 1200×630, and the battery reads the CDN file's real pixels after every deploy. The new card (scripts/make_social_card.py) must be uploaded tocdn.2plot.ai/github_assets/leaflet.2plot.dev.pngbefore this ships, orsocial_card_real_pixelsfails the deploy — deliberately.
- The network bulletin was never wired. The hub publishes announcements and
tips at
2plot.dev/api/network/bulletin, and every satellite renders them in its llms.txt viewer header. This host had nolib/bulletin.pyat all, so it showed "No announcements." and one generic tip where the hub publishes two — and an unwired host still renders both panels, which is why nobody noticed. Note thatdash_improve_my_llms/bulletin.pynever readsNETWORK_BULLETIN_URL: setting that variable without this code does nothing, silently.run.pynow prints which of the two states it booted in. - The social card was the wrong shape, and the wrong image. 1280×515 (2.49:1) is wider than both the Open Graph ideal and Twitter's 2:1 slot, so every platform cropped it — and the file was the 2plot network wordmark rather than a card for this site. Replaced with a generated 1200×630 card, and the battery now reads the served PNG's IHDR so a re-upload at a different size cannot pass while every offline test stays green.
dash-clerk-auth0.9.0 renders a dead avatar on satellites — the header control appears and never resolves the signed-in user. This host is a satellite of the 2plot.ai primary, so it is exactly the affected shape. Vendored 0.9.1.markdown2dashwas installed without--no-depsin two places —scripts/compat_matrix.pyand the README quickstart. In the matrix that meant every per-Dash-version venv booted an app with no documentation pages, so the compatibility run measured nothing.AD_APP_IDwas the package name, not the directory key. The hub listsdash-leaflet2underlegacy_idsand folds it in at ingest specifically "until leaflet's own network-standard pass setsAD_APP_ID=leaflet". It now does, andSATELLITE_APP_KEYis set alongsideSATELLITE_APP_ID.
- The Control Board appears in the nav, to admins only — its own section in
both the desktop navbar and the mobile drawer, hidden by default and revealed
server-side by the same predicate the page itself uses. The link is cosmetic:
/admin/control-boardgates itself on every render and again in its mutating callback, and fails closed without Clerk. lib/bulletin.py,scripts/make_social_card.py,tests/test_bulletin.py,tests/test_admin_nav.py, andsocial_card_real_pixelsin the battery.SITE_SHORT_NAME(withPAGE_TITLE_PREFIXderived from it rather than typed twice) andOG_IMAGE_TYPE.
-
A hosted deploy advertising
http://localhostnow says so, loudly. Production was serving/llms.txt,/sitemap.xmland every canonical link pointing athttp://localhost:8050, and nothing looked wrong: the site rendered,/healthzreturned 200, andtests/test_network_surfaces.pypassed because it asserts sitemap URLs start withBASE_URL— comparing the deployed value against itself, which is just as true when both sides are localhost. The code default was never the problem (it is alreadyhttps://leaflet.2plot.dev); a loopback value can only come fromAPP_BASE_URLorDASH_LEAFLET2_BASE_URLbeing explicitly set to one, and.env.exampleships exactly those values uncommented for local use. Three changes, none of which self-heal — auto-filling Render'sRENDER_EXTERNAL_URLwould just swap one wrong canonical origin (*.onrender.com) for another:lib.constants.base_url_misconfigured()returns an actionable message when a hosted service resolves BASE_URL to a loopback origin, naming which of the two variables is at fault;run.pyprints the resolved base URL at boot and that warning after it; and/healthznow reportsbase_url, so the origin a satellite advertises is checkable from outside it with one curl..env.examplesays plainly that its values are local-only. -
BASE_URLacceptsAPP_BASE_URLfirst, falling back to this repo'sDASH_LEAFLET2_BASE_URL. An alias, never a rename — both are set inrender.yaml, because removing one of two env names from a live service is how a host starts advertising the wrong canonical origin and deindexes itself quietly. -
dash-emoji-martandflexlayout-dashinstall from PyPI, replacing the vendored tarballs now that their working builds are published. Both keep a load-bearing floor —dash-emoji-mart>=0.0.5(0.0.3 errors on init) andflexlayout-dash>=1.1.0(1.1.0 renamed the import toflexlayout_dash, whichdocs/walking-sim/example.pyimports directly) — so a too-old resolve fails at install rather than at page render. They also re-enter CI'spip-auditjob, which skips./vendor/lines because pip-audit can only assess PyPI dists.vendor/is down to the single Clerk tarball. -
dash-clerk-auth0.9.1 → 1.0.0, andlib/auth.pystops hand-patching the satellite. Both fixes it used to inject are upstream: 0.9.1 stampsdata-clerk-domainonto the ClerkJS script tag, and 0.9.2 replaced theClerk.openSignIn()modal — which ClerkJS forbids on a satellite — with a navigation to the primary. What stays is one delegated capture-phase listener on#clerk-login-button: the package binds that id inside itsDOMContentLoadedhandler, so the header control is covered but the sign-in card inlib/page_visibility.py, which a page callback renders later, would otherwise have no listener at all. It now defers to the package's ownwindow.dashClerkAuth.buildSatelliteRedirect()(0.9.2's page-JS surface, opt-in viaCLERK_SATELLITE_SIGN_IN_REDIRECT) and falls back to the sameredirectToSignIn()call upstream makes.1.0.0 raises
requires-pythonto>=3.10—clerk-backend-api5.x publishes no 3.9 build, so the old>=3.9claim was never installable. That binds the docs site only: Docker is 3.12 and the CI docs matrix is 3.10/3.12/3.13. Thedash_leaflet2package keepsrequires-python >=3.9, which thepackage-python-rangeCI job proves against the built wheel.
Brings this satellite onto the 2plot network standard that 2plot.ai (root),
2plot.dev (hub) and dash-documentation-boilerplate (the template) now ship.
No dl2.* component changed; everything here is the documentation site, its
analytics and its CI.
- Every page shipped an empty
og:image. Dash emitsog:imageandtwitter:imagefor each page and leaves themcontent=""when it can find no image, which unfurls as a blank preview card on Facebook, Twitter/X, Slack, Discord and LinkedIn — strictly worse than declaring no image at all.register_page(image_url=...)now supplies the real absolute URL, served from the 2plot CDN so a sleeping free-tier container never costs a preview.templates/index.htmldeliberately declares only the auxiliaries Dash omits (og:image:width/height/alt/type/secure_url,twitter:image:alt), so it cannot duplicate the URL. - The web app manifest could never have offered an install. Its
nameandshort_namewere empty strings — which disqualifies a manifest outright — and its iconsrcpaths pointed at/android-chrome-192x192.pngat the site root, where nothing is served; the files live under/assets/favicon_io/. Nothing linked to it either. Fixed, linked, and joined byapple-touch-icon(iOS ignores the manifest and uses that for Add to Home Screen) and themsapplication-*tiles. - Crawler traffic was never counted. The per-request tracker was a Flask
before_requesthandler registered afteradd_llms_routes, and dash-improve-my-llms' bot middleware answers every crawler with prerendered HTML — which short-circuits the remainingbefore_requesthandlers. No crawler request ever reached the ledger, so this site reportedbot_hits: 0to 2plot.ai structurally, for every day it has been live, with nothing visibly broken. The tracker now wraps the WSGI/ASGI callable instead (_wsgi_tracker/_asgi_tracker), which sits outside the whole application and cannot be short-circuited. Registration order was not a usable fix: Flask runsbefore_requesthandlers first-registered-first, while Starlette makes the last-added middleware outermost, so no single ordering is correct on all three backends. - The ad fetch and the traffic rollup polluted the hub's ledgers. Both
server-to-server calls left as
python-requests/2.x, which 2plot.dev and 2plot.ai classify as a bot — so every docs page view here inflated the hub'sbot_hits. Both now send the network's internal-traffic User-Agent. - A control-board toggle could rename the site.
apply_llms_statere-registers a page's metadata whenever a visibility verdict changes, using the name the markdown loader recorded —"Home"for this site's root. One flip of the home page's llms.txt switch would have overwritten the site brand at runtime, silently degrading the published identity to a generic word.lib.page_visibility.published_namenow pins the root toSITE_BRAND. - gunicorn was pinned under a security floor.
gunicorn>=21.2,<22was holding the production server on a line carrying two HTTP request-smuggling CVEs (CVE-2024-6827, CVE-2024-1135), becausemarkdown2dash0.1.2 declaresgunicorn<22. markdown2dash is now installed with--no-deps(its real dependencies moved intorequirements.txt, carrying its own version ranges) and the floor isgunicorn>=23.0.0, asserted inside the built image by CI.
- Explicit site identity.
lib.constants.SITE_BRAND— "dash-leaflet2 — Leaflet 2 maps for Dash" — is now the one string on every surface:Dash(title=),register_page_metadata(path="/"), the home markdown's H1 and the README. This matters because the home page is registered as"Home", whichresolve_site_titleskips as generic; without the explicit registration the site published a framework fallback. - An introduction video on the home page and in the README —
Dash Leaflet 2.0: Drone Tracking, Image Overlays & Map Packages in
Python. Embedded from
youtube-nocookie.com, so the player sets no visitor-tracking cookies on a site that otherwise counts nothing beyond an anonymised page view, and accompanied by a plain link — an agent reading/llms.txtnever sees an iframe, and neither does anyone whose browser blocks the embed. scripts/network_smoke.py— the network's named-check battery, run against the CI container and against production with identical check names. Proves identity, the agent-facing document surfaces, the robots fingerprint, hidden-page 404s and content negotiation.scripts/smoke_live.py— post-deploy checks: every canonical, every crawler body, and every peerllms.txtin the directory. Peer failures warn rather than fail, because gating a deploy on somebody else's certificate is shared fate.tests/— a secretless in-process suite (80 tests) covering site identity, the internal-traffic contract in both directions, the agent and crawler surfaces, the social card and manifest, and the smoke scripts themselves, so a battery that has rotted into a silent pass fails here first.- Two live battery checks for the surfaces above —
social_card_is_shareable(the image is declared once, is not empty, and actually resolves) andinstallable_as_an_app(the manifest is linked, named, and its icons resolve). Both fail invisibly in production otherwise: nobody sees their own link previews, and no browser explains why it declined to offer an install. .github/workflows/cd.yml— deploy plus live verification, waiting for five consecutive healthy responses after a 120s settle rather than a single 200 (Render swaps instances, so the old build answers throughout)..github/dependabot.yml— weekly pip with adash-networkgroup, weekly npm, monthly actions and Docker.
dash-improve-my-llms>=2.3.4(from 2.3.3), the network floor: 2.3.4 addsresolve_site_title, without which the/llms.txtH1 and the llms viewer's brand chip fall back toapp.title.- CI on the network baseline:
permissions: contents: read,timeout-minuteson every job, anactionlintstep (an invalid workflow file is the one defect CI structurally cannot report), a real Docker build → boot → battery job with buildx GHA caching, version fingerprints asserted inside the image, and an advisorypip-audit. CI now runs on pull requests andworkflow_callonly —mainbelongs to CD, which calls it. The existing wheel and Dash-compatibility jobs are unchanged. - The home page is no longer the generated scaffold: it opens with the site brand and describes what the library actually is.
templates/index.htmlno longer publishespip-install-python.comas this site's Organization URL, author URL or footer link — it is not a 2plot network host. Those now point at https://github.com/2plotai.- The README's assets are served from
cdn.2plot.airather thanraw.githubusercontent.com, so they render on PyPI (where the README is the long description) as well as on GitHub.
First public release: the project splits into a private R&D checkout and this public mirror, which is what ships to PyPI and to https://leaflet.2plot.dev.
The project is split into a private R&D checkout and this public mirror, which is what ships to PyPI and to https://leaflet.2plot.dev.
/tile-selectorrewritten as a lean, self-contained page documenting thedl2.TileSelectorcomponent — click / shift-drag selection, the{z, x, y, url, bounds}data boundary, and the[MUTABLE]round-trip that lets a Clear button writeselectedTilesback from Python. The previous 3,700-line AI tile-generation lab stays internal.scripts/smoke_test.py— headless suite driving the app through the backend's test client (no socket, no browser): page registration with duplicate-path detection, layout construction plus JSON serialisation of every example, and an HTTP sweep of every route,/_dash-layout,/_dash-dependencies,/healthz,/llms.txt,/robots.txtand/sitemap.xml.scripts/compat_matrix.py— builds a throwaway virtualenv per Dash version (4.1.0 / 4.2.0 / 4.3.0 / 4.4.1 by default), installs the docs site into each, runs the smoke suite, and writesCOMPATIBILITY.md. Optional--browserleg boots each venv for real and collects console errors with Playwright. This is what turns thedash>=4.1claim into evidence.scripts/sync_from_rnd.py— pulls R&D work forward into the mirror behind an explicit denylist. Pull, not push: a new R&D docs page surfaces as NEW for approval rather than leaking by being forgotten upstream.- 2plot network integration, all dormant without environment keys:
lib/ad_client.py(2plot.dev ad slots in the docs aside),lib/satellite_analytics.py(signed traffic rollups to 2plot.ai,/healthz, SPA page-view beacon),lib/auth.py(Clerk satellite of the 2plot.ai primary, including the two dash-clerk-auth 0.9.0 satellite fixes), andlib/page_visibility.py+pages/control_board.py(four-tier page visibility re-checked on every render, editable live at/admin/control-board). - Deployment:
Dockerfile,.dockerignore,render.yamlandDEPLOYMENT.mdforleaflet.2plot.dev. vendor/— the two docs-only packages that are not on PyPI (dash_emoji_mart0.0.5,flexlayout_dash1.1.0) are committed here sopip install -r requirements.txtworks from a clean clone. Neither is needed by thedash_leaflet2package, which still requires onlydash>=4.1.
app.py→run.py, withHOST/PORT/DASH_DEBUGread from the environment so the compatibility matrix can run several Dash versions side by side.requirements.txtrewritten: the vendored packages install from relative./vendor/paths instead of absolutefile:///Users/...URLs, and the Dash pin carries a# COMPAT-MATRIX: dashtag the matrix script strips per run.- README rebuilt for the public release;
pyproject.tomlgained full trove classifiers and project URLs pointing at the documentation site.
0.1.0 — 2026-07-04
dl2.TileLayer and dl2.ImageOverlay surface Leaflet's crossOrigin option
("anonymous" | "use-credentials" | ""). Setting it makes the underlying <img>
loads CORS-mode so canvas captures (map screenshots / html2canvas thumbnails) can
read the pixels without tainting the canvas — requested by SailsBoard's
save-time-thumbnail pipeline. Opt-in with no default: a CORS-mode img fails to
load entirely against a host that doesn't answer Access-Control-Allow-Origin, so
leave it unset for tile providers you don't control. Construction-time only (for
ImageOverlay, setUrl and the editable drag/resize/rotate transforms reuse the
same img element, so the attribute set at construction persists).
Closes the surface gap downstream projects (SailsBoard's harbor map being the
canonical one) hit when migrating off dash-leaflet 1.x. Every item below is
verified end-to-end in a new live showcase page under /docs/<slug>/.
TileLayer pro props — dl2.TileLayer gains minZoom, bounds, errorTileUrl,
zIndex, subdomains, detectRetina, tms. opacity + zIndex are [MUTABLE]
via setOpacity / setZIndex. Lets downstream apps clip tile requests to a
geographic box, hide 404 tiles with a transparent PNG, stack multiple tile layers
explicitly, and shard CDN load across {s} subdomains. Showcase: /tilelayer-pro-props.
Map pro props — dl2.Map gains minZoom, maxZoom, maxBounds, zoomControl,
keyboard, plus the 6 interaction-disable handlers: dragging, scrollWheelZoom,
doubleClickZoom, boxZoom, pinchZoom (v2's name for v1's touchZoom), and
tapHold. All of zoom/bounds/keyboard/the 5 user-flippable handlers are [MUTABLE]
— a callback can lock dragging while a walkthrough plays, kill scroll-wheel zoom in a
detail-preview panel, etc. pinchZoom writes through to both pinchZoom (v2) and
touchZoom (v1 alias) so the prop name stays stable as Leaflet 2 evolves. Showcase:
/map-pro-props.
GeoJSON clustering — dl2.GeoJSON adds the dash-leaflet 1.x clustering surface
backed by SuperCluster v8: cluster,
superClusterOptions, pointToLayer, clusterToLayer, hideout,
zoomToBoundsOnClick, spiderfyOnMaxZoom. pointToLayer / clusterToLayer accept
a JS source string compiled via new Function(...) at construction time; both
receive a ctx = { hideout, leaflet, map } argument so user code can build any
Leaflet 2 layer without depending on a global. Non-point features (LineString,
Polygon) pass through unclustered. Showcase: /geojson-cluster.
LayerGroup + FeatureGroup — new dl2.LayerGroup and dl2.FeatureGroup
components. Children of either attach to the group instead of the map via a
forwarding LeafletMapContext proxy (makeForwardingMapProxy in
layersControl-shared.ts) that intercepts addLayer/removeLayer but transparently
forwards every other map method (latLngToLayerPoint, on, getCenter, ...) to the
real map — required for layers like Marker whose rotation effect needs the real
projection. FeatureGroup additionally emits a combined geojson of its vector
children plus an aggregate n_clicks and n_layers counter. Showcase: /layer-group.
ScaleControl — dl2.ScaleControl wraps Leaflet 2's Control.Scale (lives on the
Control namespace but is not ESM-exported by leaflet@2.0.0-alpha.1, so we reach
through (Control as any).Scale). Props: position (mutable), metric, imperial,
maxWidth, updateWhenIdle. Showcase: /scale-fullscreen-image.
FullScreenControl — dl2.FullScreenControl is a thin custom Control that wraps
the browser's native requestFullscreen() / exitFullscreen() API around the map
container (Leaflet 2 itself does not ship a fullscreen control). Round-trips
fullscreen (boolean) and n_clicks to Dash so a callback can react when the user
enters or leaves fullscreen. Showcase: /scale-fullscreen-image.
ImageOverlay — dl2.ImageOverlay wraps leaflet.ImageOverlay. Mutable url,
bounds, opacity, zIndex; optional interactive=True lets the image fire
n_clicks. Useful for previewing a raster scan before slicing it into tiles, draping
a single static image onto a geographic box, or showing a non-tiled overlay. Showcase:
/scale-fullscreen-image.
- Editable transform controls — set
editable=Truefor a TextMarker-style control system: click to select, drag the body to move (translatesbounds), drag the corner dot to resize (scalesboundsabout theanchor, which stays pinned), and the top dot to rotate (a CSS-transform visual rotation pivoting at theanchor—boundsstay axis-aligned since Leaflet's ImageOverlay has no native geo-rotation). The white anchor dot sits at the chosenanchor. New propseditable,selected(two-way),rotation(two-way),anchor;boundsbecomes two-way andn_transformscounts move/resize commits.
TextMarker — new dl2.TextMarker: editable, draggable, styleable text placed on
the map like a Marker (implements Route A of the text-caption-marker-proposal). It is
a Leaflet 2 Marker whose icon is a content-sized, optionally-contentEditable text box
rendered into the icon via a React portal, reusing Marker's drag lifecycle + the
transform-reprojection trick (so rotation survives Leaflet's constant transform rewrites).
- Placement: anchored to
[lat, lng]; whenpositionis omitted it spawns at the center of the current viewport and writes that position back.anchor(9 positions) picks which point of the box sits on the latlng — that point is also the rotation pivot. - Direct manipulation: drag to move (writes
position+n_drags), double-click to edit inline (writestext+n_edits), and whenselecteda corner resize handle (→fontSize) and a rotate handle (→rotation, Shift-snaps to 15°) appear. - Style:
color,backgroundColor,fontFamily,fontSize,fontWeight,fontStyle,padding,borderRadius,rotation,rotateWithMap— all[MUTABLE]two-way (the contextual glass toolbar that shows while selected edits them and round-trips every change to Dash, so a host can also drive style from props).selectedis two-way (clicking the label selects it; a map-background click deselects);showToolbarhides the built-in toolbar for hosts that supply their own. - Two size models via
scaleWithZoom(+referenceZoom):false(default) is a constant screen-size HUD caption (fontSizeis literal px at every zoom);trueis geographic sizing — the on-screen size scales by2^(zoom − referenceZoom)so the caption keeps a fixed ground footprint as the camera flies.referenceZoomdefaults to the zoom at which the label was created. - Anchor offset is applied via the icon's margin (not baked into the transform) so
Leaflet's own mid-drag positioning and ours never disagree; a post-handle-drag
clickis swallowed so resize/rotate don't deselect. - The white selection dot (which doubles as the resize grip) is drawn at the chosen
anchorpoint —bottom→ bottom-center,top-left→ top-left, … (center→ bottom-right so it never covers the text) — so you can see where the label is pinned. Resize now references the box center (the dot sits at the anchor, so an anchor-referenced ratio would divide by ~0). selectedis uncontrolled when omitted: the marker self-manages selection (click to select, two-stage map-click to deselect) and a map-event bus keeps only one TextMarker selected at a time. Pass an explicitselectedto drive it from the host. The anchor model- dot positioning now live in the shared
src/ts/anchor.ts(used by the editable ImageOverlay too).
- dot positioning now live in the shared
EditControl text tool (proposal Route B) — dl2.EditControl gains a text tool
alongside marker / polyline / polygon / …. Picking it and clicking the map drops an
inline-editable caption that round-trips through the same geojson channel as every other
shape — a GeoJSON Point carrying kind:"text" + the caption style (text, color,
fontSize, fontFamily, fontWeight) in properties. In edit mode the caption is draggable
and double-click re-opens the inline editor; cancel/revert rebuilds captions as text icons (not
pins). Enable per-tool with draw={"text": True}. Showcase: /text-marker.
Five new markdown-driven showcase pages, each with a focused "limited working
example" example.py next to the markdown:
/tilelayer-pro-props— two stacked tile layers (OSM base withsubdomains+detectRetina; CARTO labels-only overlay clipped to a Rockport, TXboundsbox with a transparenterrorTileUrl). Sliders driveopacity+zIndexlive./map-pro-props— 6-handler Switch panel + zoom RangeSlider +maxBoundstoggle- live viewport readback. Flipping a Switch immediately disables the matching Leaflet handler on the live map.
/geojson-cluster— 200 synthetic vessel positions colored by category via a JSpointToLayerreading a Python-shippedhideoutcolor map; cluster bubbles take the dominant category's color. Cluster-radius slider tunessuperClusterOptions.radiuslive./layer-group— two maps: oneLayerGroupof three markers behind a single Switch (the whole group toggles together), oneFeatureGroupwrapping four shapes and emitting combinedgeojson+ bumpingn_clickson any child click./scale-fullscreen-image— one map with the scale bar (position + metric/imperial Switches), the fullscreen button (reportsfullscreen+n_clicks), and a swappableImageOverlaywith opacity slider./text-marker— a selectedTextMarkeryou drag / edit / resize / rotate / restyle on the map (or drive from the right column: text, color, font size, rotation, anchor,scaleWithZoom), a second caption withscaleWithZoom=Truethat holds its ground size, and theEditControltexttool wired in (click the T, click the map, type — the caption shows up inEditControl.geojsonas akind:"text"Point). Live readback panel.
- New runtime dependency:
supercluster@^8.0.1(bundled intodash_leaflet2.js). The 0.0.1 wheel sat at ~261 KiB; with clustering + the four new components the bundle is now ~263 KiB. src/ts/types/leaflet.d.ts— extended forMap.setMinZoom/setMaxZoom/setMaxBounds/getMinZoom/getMaxZoom,Map.keyboard,TileLayer.setOpacity/setZIndex,ImageOverlay, plus a minimal ambientsuperclustermodule.src/ts/layersControl-shared.ts— newmakeForwardingMapProxy(onAdd, onRemove, getRealMap)builds a JS-Proxy-based map stand-in that interceptsaddLayer/removeLayerbut forwards every other property access to the real map. Used byLayerGroupandFeatureGroup; the existing thinmakeMapProxyis kept forBaseLayer/Overlaywhere forwarding is unwanted.src/ts/theme.css— cluster-bubble glass styling (.dl2-cluster-bubbleand.dl2-cluster-{32,40,48,56}sizes) + fullscreen-button styling (.dl2-fullscreen-control,.dl2-fullscreen-button).
dl2.TileSelector— a map control that turns the map into a tile picker: click or shift-drag to select tiles, which round-trip to Python as{z, x, y, url, bounds}, keyed byz/x/yso selections survive pan and zoom.- Compare Lab (
/compare-lab) — tileset comparison surface: anEasyButton+ Popover +dash_mui_charts.TreeViewProdriving a clientside reconciler over a stack ofTileLayeroverlays (visibility, opacity, z-order, deletion), seeded with synthetic SVG overlays so every interaction responds in under a second. - Walking Sim (
/walking-sim) — Esri Imagery + NatGeo layered basemaps with a street-tile minimap; flyTo between WALK / EXPLORE modes. - Sub-toolbar + live drawing feedback in
dl2.EditControl— vertex-handle previews, cursor-following guide tooltip, dashed rubber-band, context-sensitive fly-out actions (Finish / Delete-last-point / Cancel during draw; Save / Cancel during edit).
- Cross-zoom prompt engineering — the AI was pasting descendant references as visible rectangular insets with duplicated features and a seam. Rewrote SOURCE + CROSS-ZOOM REFERENCE labels and the addendum to forbid pasting/insets and to assert the source tile as the geometric ground truth for all four quadrants.
- Tileset comparison overlay layering — z15 (later-added, larger) was covering z16
at every viewport zoom. Added zoom-meets-tile filtering: among overlapping tree-checked
tiles, only the deepest zoom the viewport has met shows (
z15at vz=13–15,z16at vz=16,z17at vz=17+). Standalone tiles unaffected. SetzIndex = 400 + tileZso any transient overlap keeps the finer tile on top. - EasyButton popover toggle —
dmc.Popover.openedis not pushed throughsetPropsafter internal state changes; switched to a DOM-read clientside pattern reading.mantine-Popover-dropdownoffsetParent. MUI TreeViewoverlay flicker — refactored to "mount-everything-hide-via-opacity" withtransition: opacity 120ms ease; checkbox-row double-click bounces no longer tear overlays off the map.
0.0.1 — 2026-05-22
First alpha release. Build a wheel from source (python -m build); not yet on PyPI.
| Component | Wraps | Notes |
|---|---|---|
dl2.Map |
leaflet.Map |
viewport + clickData round-trip; React-context bridge replaces react-leaflet |
dl2.TileLayer |
leaflet.TileLayer |
url, attribution, maxZoom, opacity |
dl2.Marker |
leaflet.Marker |
default / icon / emoji / iconify / full iconOptions icon modes; bundled marker images (base64) |
dl2.Polyline, dl2.Polygon, dl2.Rectangle, dl2.Circle, dl2.CircleMarker |
corresponding leaflet.* |
vector path props + click round-trip |
dl2.GeoJSON |
leaflet.GeoJSON |
data, style, clickFeature; pointToLayer sets the bundled default icon to dodge v2's stale Icon.Default() trap |
dl2.Popup, dl2.Tooltip |
leaflet.Popup, leaflet.Tooltip |
render arbitrary Dash content through React portals |
dl2.LayersControl + dl2.BaseLayer + dl2.Overlay |
custom (Control subclass) |
v2's Layers class is not ESM-exported; ships our own with RegisterContext |
dl2.EditControl |
native v2 toolbar Control |
leaflet-draw is v1-only; our native replacement draws marker / polyline / polygon / rectangle / circle + delete with GeoJSON round-trip |
dl2.EasyButton |
leaflet.Control |
Iconify icon, n_clicks / n_dblclicks |
dl2.AttributionControl |
leaflet.Control.Attribution |
prefix, custom attribution |
dl2.KeyboardControl |
custom (Control) |
DOM key listeners → lastKey / n_events |
dl2.MiniMap |
custom (Control) |
second leaflet.Map instance pinned to a corner |
dl2.TileSelector |
custom (Control) |
hover-highlight, click-toggle, shift+drag box-select; selectedTiles round-trip with {z, x, y, url, bounds} |
dl2.Tooltip, dl2.Popup |
(see above) | bind to any layer via React portal |
- 20+ pages under
docs/demonstrating v2 features through thedash.hooksAPI with no build step: pointer events, canvas overlay, ES6 subclassing,ResizeObserversizing, vector layers, emoji/iconify markers, layers control, draw + edit + measure, easy button, MiniMap, basic rotation, flight sim, walking sim, events→Python, flyTo, attribution control, tile-layers-pro, tile-selector, compare-lab. - DMC AppShell + sidebar + dark-mode toggle; FastAPI backend by default
(
DASH_BACKEND=flask python run.pyto fall back).
.claude/directory: 1 subagent (leaflet2-component-author), 2 skills (build-and-verify,new-component), 3 path-scoped rules (leaflet2-v2-api.md,dash-components.md,showcase-pages.md).- Webpack +
dash-generate-componentsbuild pipeline; Python classes generated from TS JSDoc; default marker icons inlined as base64 to dodge v2's CSS-path detection.
- v2's UMD global is
window.leaflet(notwindow.L). - No lowercase factories —
new Marker(...), notL.marker(...). - v2 fires pointer events (
pointermove/pointerdown), not mouse events. BlanketOverlay._onMoveEnd()clears the canvas after drawing — Canvas renderer workaround:requestAnimationFrame(() => renderer._update())aftermoveend/zoomend.- v2 ships no TypeScript types — minimal ambient declarations at
src/ts/types/leaflet.d.ts.