From 9fae876516fa1fd157e42a4e942611c5658ed821 Mon Sep 17 00:00:00 2001 From: codecalm Date: Sun, 4 Oct 2026 17:20:02 +0200 Subject: [PATCH] Restyle the maps after mapcn and add a 3D globe demo --- .changeset/map-look.md | 7 + core/scss/_variables.scss | 2 +- .../css-var-prefix.test.mjs.snap | 1 + core/scss/vendor/_maplibre.scss | 73 +++++----- docs/content/ui/components/map.mdx | 87 +++++++++--- preview/pages/map-fullsize.astro | 2 +- preview/pages/maps.astro | 125 ++++++++++++++---- shared/components/demo/Map.astro | 28 ++-- shared/data/maps.json | 4 +- 9 files changed, 243 insertions(+), 86 deletions(-) create mode 100644 .changeset/map-look.md diff --git a/.changeset/map-look.md b/.changeset/map-look.md new file mode 100644 index 000000000..70fc902e8 --- /dev/null +++ b/.changeset/map-look.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/preview": minor +"@tabler/docs": patch +--- + +Updated the map look: gray basemap, dot markers, popups with no arrow, `.map-tooltip`, `.map-marker-label`, and a 3D globe demo. diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index d078ece95..03d0e2b8a 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -1281,7 +1281,7 @@ $ribbon-bookmark-notch-width: 0.5rem !default; // scss-docs-start map-marker-variables $map-marker-size: 1.75rem !default; -$map-marker-dot-size: 0.875rem !default; +$map-marker-dot-size: 1rem !default; $map-marker-bg: var(--primary) !default; $map-marker-color: var(--primary-fg) !default; $map-marker-border-width: 2px !default; diff --git a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap index 92074577e..c9f4a6477 100644 --- a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap +++ b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap @@ -52,6 +52,7 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre "--tblr-border-color-translucent", "--tblr-border-radius", "--tblr-border-radius-pill", + "--tblr-border-radius-sm", "--tblr-border-style", "--tblr-border-width", "--tblr-card-bg", diff --git a/core/scss/vendor/_maplibre.scss b/core/scss/vendor/_maplibre.scss index bb7df8d60..600249ccd 100644 --- a/core/scss/vendor/_maplibre.scss +++ b/core/scss/vendor/_maplibre.scss @@ -65,9 +65,9 @@ border-color: var(--body-color); } -// Popup -.maplibregl-popup-content { - padding: 0.75rem 1rem; +// Popup: a small bordered panel with no arrow +.maplibregl-popup .maplibregl-popup-content { + padding: 0.75rem; color: var(--body-color); background: var(--bg-surface); box-shadow: @@ -76,21 +76,25 @@ @include border-radius(var(--border-radius)); } +.maplibregl-popup-tip { + display: none; +} + // Room for the close button, which MapLibre pins to the right edge in both directions -.maplibregl-popup-content:has(.maplibregl-popup-close-button) { +.maplibregl-popup .maplibregl-popup-content:has(.maplibregl-popup-close-button) { /* rtl:ignore */ - padding-right: 2.25rem; + padding-right: 2rem; } .maplibregl-popup-close-button { - width: 1.5rem; - height: 1.5rem; + width: 1.25rem; + height: 1.25rem; padding: 0; - margin: 0.375rem; + margin: 0.25rem; font-size: 1rem; line-height: 1; color: var(--secondary); - @include border-radius(var(--border-radius)); + @include border-radius(var(--border-radius-sm)); &:hover { color: var(--body-color); @@ -98,32 +102,17 @@ } } -.maplibregl-popup-anchor-top, -.maplibregl-popup-anchor-top-left, -.maplibregl-popup-anchor-top-right { - .maplibregl-popup-tip { - border-bottom-color: var(--bg-surface); - } -} - -.maplibregl-popup-anchor-bottom, -.maplibregl-popup-anchor-bottom-left, -.maplibregl-popup-anchor-bottom-right { - .maplibregl-popup-tip { - border-top-color: var(--bg-surface); - } -} - -.maplibregl-popup-anchor-left .maplibregl-popup-tip { - border-right-color: var(--bg-surface); -} - -.maplibregl-popup-anchor-right .maplibregl-popup-tip { - border-left-color: var(--bg-surface); +// Popup used as a tooltip: a short inverted label, shown on hover +.map-tooltip .maplibregl-popup-content { + padding: 0.25rem 0.5rem; + color: var(--text-inverted); + pointer-events: none; + background: var(--bg-surface-inverted); + box-shadow: #{$box-shadow-md}; } // Popup that holds a card-like block: an image on top, padded content below -.map-popup-card { +.map-popup-card.maplibregl-popup { .maplibregl-popup-content { width: #{$map-popup-card-width}; padding: 0; @@ -161,7 +150,6 @@ line-height: 1; color: var(--map-marker-color); cursor: pointer; - filter: #{$map-marker-shadow}; &::before { position: absolute; @@ -169,6 +157,7 @@ z-index: -1; content: ''; background: var(--map-marker-bg); + filter: #{$map-marker-shadow}; border: var(--map-marker-border-width) solid var(--map-marker-border-color); @include border-radius(var(--border-radius-pill)); } @@ -187,6 +176,24 @@ --map-marker-size: #{$map-marker-dot-size}; } +// Short text under a marker +.map-marker-label { + position: absolute; + top: 100%; + left: 50%; + margin-top: 0.125rem; + font-size: $h6-font-size; + font-weight: var(--font-weight-medium); + line-height: 1.2; + color: var(--body-color); + text-shadow: + 0 0 2px var(--bg-surface), + 0 0 2px var(--bg-surface), + 0 0 4px var(--bg-surface); + white-space: nowrap; + transform: translateX(-50%); +} + // Teardrop: a square with one sharp corner, turned to point down. Its tip // reaches 20.7% below the circle, so the root is that much taller. .map-marker-pin { diff --git a/docs/content/ui/components/map.mdx b/docs/content/ui/components/map.mdx index d4ba7294c..e9ee709cd 100644 --- a/docs/content/ui/components/map.mdx +++ b/docs/content/ui/components/map.mdx @@ -12,6 +12,9 @@ classnames: desc: Wrapper that keeps the map at a fixed aspect ratio - class: map-marker desc: Marker drawn in HTML, a circle that can hold an icon, a count or an avatar + part: + - class: map-marker-label + desc: Short text under a marker modifier: - class: map-marker-dot desc: Small dot with no content @@ -19,6 +22,8 @@ classnames: desc: Teardrop pin that points down, used with the bottom anchor - class: map-popup-card desc: Popup with no padding and a fixed width, for an image above the text + - class: map-tooltip + desc: Popup shown as a small dark label, for text on hover color: - class: map-marker-{color} desc: Any theme color, for example map-marker-red or map-marker-green @@ -83,7 +88,7 @@ Create the map after the DOM is ready. Pass the id of your container, a style, a document.addEventListener('DOMContentLoaded', function () { const map = new maplibregl.Map({ container: 'map-simple', - style: 'https://tiles.openfreemap.org/styles/liberty', + style: 'https://tiles.openfreemap.org/styles/positron', zoom: 13, center: [13.4049, 52.518827], }) @@ -102,10 +107,10 @@ Set `style` to the URL of any MapLibre style. OpenFreeMap has five of them: | Style | Result | | --- | --- | -| `https://tiles.openfreemap.org/styles/liberty` | Default street map. | +| `https://tiles.openfreemap.org/styles/positron` | Light gray map, used by the Tabler demos. Markers and data stand out on it. | +| `https://tiles.openfreemap.org/styles/dark` | Dark gray map, used by the demos in dark mode. | +| `https://tiles.openfreemap.org/styles/liberty` | Street map in color. | | `https://tiles.openfreemap.org/styles/bright` | Street map with stronger colors. | -| `https://tiles.openfreemap.org/styles/positron` | Light gray map, good under charts and markers. | -| `https://tiles.openfreemap.org/styles/dark` | Dark map. | | `https://tiles.openfreemap.org/styles/fiord` | Dark blue map. | @@ -116,7 +121,7 @@ Styles from other providers work the same way, as long as they follow the MapLib ### Markers -Add a marker for each place. `setLngLat` takes the same longitude and latitude order as `center`. +Add a marker for each place. `setLngLat` takes the same longitude and latitude order as `center`. The demos use a small dot, drawn with the `.map-marker` and `.map-marker-dot` classes. ```js const cities = [ @@ -126,7 +131,10 @@ const cities = [ ] cities.forEach(function (coords) { - new maplibregl.Marker({ color: 'var(--tblr-primary)' }).setLngLat(coords).addTo(map) + const element = document.createElement('span') + element.className = 'map-marker map-marker-dot' + + new maplibregl.Marker({ element: element }).setLngLat(coords).addTo(map) }) ``` @@ -134,7 +142,7 @@ cities.forEach(function (coords) { -The `color` option is written to the `fill` attribute of the marker icon. A CSS variable works there, so `var(--tblr-primary)` keeps the markers in your theme color, including after a theme change. +Without `element`, MapLibre draws its own pin. Its `color` option accepts a CSS variable, for example `var(--tblr-primary)`. ### Custom markers @@ -148,6 +156,8 @@ element.textContent = '12' new maplibregl.Marker({ element: element }).setLngLat([13.35, 52.512]).addTo(map) ``` +Put a `.map-marker-label` element inside the marker for a short text under it. + Add `.map-marker-dot` for a small dot, or `.map-marker-pin` for a pin that points down. A pin needs `anchor: 'bottom'`, so its tip sits on the place. Change the color with `.map-marker-{color}`. ```js @@ -161,7 +171,7 @@ new maplibregl.Marker({ element: pin, anchor: 'bottom' }).setLngLat([13.47, 52.5
-
+
Store
12
@@ -201,15 +211,32 @@ marker.on('dragend', function () { ### Popups -Attach a popup to a marker with `setPopup`. It opens when the marker is clicked. Click a marker on the map above to see one. +Attach a popup to a marker with `setPopup`. It opens when the marker is clicked. Click a marker on the map above to see one. Tabler draws the popup as a small bordered panel with no arrow. ```js -new maplibregl.Marker({ color: 'var(--tblr-primary)' }) +new maplibregl.Marker({ element: element }) .setLngLat([16.366667, 48.2]) - .setPopup(new maplibregl.Popup({ offset: 32 }).setText('Vienna')) + .setPopup(new maplibregl.Popup({ offset: 16, closeButton: false }).setText('Vienna')) .addTo(map) ``` +Leave out `closeButton: false` to show a close button in the corner. The popup also closes on a click on the map. + +### Tooltips + +For a short label on hover, add the `map-tooltip` class to a popup. It turns the popup into a small dark label. + +```js +const tooltip = new maplibregl.Popup({ closeButton: false, offset: 12, className: 'map-tooltip' }) + +map.on('mousemove', 'cities', function (event) { + tooltip.setLngLat(event.lngLat).setText(event.features[0].properties.name).addTo(map) +}) +map.on('mouseleave', 'cities', function () { + tooltip.remove() +}) +``` + Use `setHTML` instead of `setText` for richer content. Only pass HTML you trust, as it is not escaped. ### Popup card @@ -267,7 +294,7 @@ A map style does not change with the page theme on its own. Pick the style from ```js const themeStyle = function () { const dark = document.documentElement.getAttribute('data-bs-theme') === 'dark' - return 'https://tiles.openfreemap.org/styles/' + (dark ? 'dark' : 'liberty') + return 'https://tiles.openfreemap.org/styles/' + (dark ? 'dark' : 'positron') } const map = new maplibregl.Map({ @@ -299,7 +326,7 @@ map.on('style.load', function () { type: 'line', source: 'route', layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': '#066fd1', 'line-width': 4 }, + paint: { 'line-color': '#066fd1', 'line-width': 3, 'line-opacity': 0.8 }, }) }) ``` @@ -330,7 +357,7 @@ function mapColor(name) { return 'rgb(' + pixel[0] + ', ' + pixel[1] + ', ' + pixel[2] + ')' } -map.addLayer({ id: 'route', type: 'line', source: 'route', paint: { 'line-color': mapColor('--tblr-primary'), 'line-width': 4 } }) +map.addLayer({ id: 'route', type: 'line', source: 'route', paint: { 'line-color': mapColor('--tblr-primary'), 'line-width': 3 } }) ``` Call it inside `style.load`, so the colors are read again after a theme change. @@ -378,12 +405,40 @@ map.addLayer({ source: 'cities', paint: { 'circle-color': '#066fd1', - 'circle-opacity': 0.4, - 'circle-radius': ['interpolate', ['linear'], ['get', 'users'], 0, 4, 1600, 28], + 'circle-opacity': 0.6, + 'circle-radius': ['interpolate', ['linear'], ['get', 'users'], 0, 4, 1600, 24], }, }) ``` +### Content over the map + +To put buttons or a small card over the map, place them next to the map container and position them with the utility classes: + +```html +
+
+
+
+ + +
+
+
+``` + +### Globe + +MapLibre can draw the map as a 3D globe. Set the projection in the `style.load` event, as it is part of the map style. People can turn the globe by dragging it. + +```js +map.on('style.load', function () { + map.setProjection({ type: 'globe' }) +}) +``` + +Use a low zoom, between 1 and 2, to see the whole globe. To connect two places with an arc, draw a line layer through points on the shortest path between them. The [maps preview page](https://preview.tabler.io/maps.html) has a short function that returns these points. + ### Moving the map Move to another place with `flyTo`, for example from a row of buttons: diff --git a/preview/pages/map-fullsize.astro b/preview/pages/map-fullsize.astro index bc2ee098f..a9ab656ce 100644 --- a/preview/pages/map-fullsize.astro +++ b/preview/pages/map-fullsize.astro @@ -12,7 +12,7 @@ import CaptureScript from '@shared/components/CaptureScript.astro' window.tabler_map ??= {} function initMap() { - const themeStyle = () => `https://tiles.openfreemap.org/styles/${document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'liberty'}` + const themeStyle = () => `https://tiles.openfreemap.org/styles/${document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'positron'}` const map = new maplibregl.Map({ container: 'map-fullsize', diff --git a/preview/pages/maps.astro b/preview/pages/maps.astro index 853f21a2a..64f0f1780 100644 --- a/preview/pages/maps.astro +++ b/preview/pages/maps.astro @@ -61,6 +61,14 @@ const storeCities: [number, number][] = [ ] const stores = storeCities.flatMap(([lng, lat], city) => Array.from({ length: 5 + (city % 4) }, (_, index) => [lng + Math.sin(city * 7 + index * 3) * 0.6, lat + Math.cos(city * 5 + index * 2) * 0.4])) +const hub = { name: 'London', lng: -0.1276, lat: 51.5072 } +const destinations = [ + { name: 'Tokyo', lng: 139.6917, lat: 35.6895 }, + { name: 'Mumbai', lng: 72.8777, lat: 19.076 }, + { name: 'Singapore', lng: 103.8198, lat: 1.3521 }, + { name: 'Nairobi', lng: 36.8219, lat: -1.2921 }, +] + const places = [ { name: 'Museum Island', text: 'Five museums on the Spree, open daily.', photo: 'a-visit-to-the-bookstore.jpg', lat: 52.5169, lng: 13.4019, open: true }, { name: 'Bellevue Palace', text: 'Residence by the Tiergarten park.', photo: 'book-on-the-grass.jpg', lat: 52.5176, lng: 13.3531, open: false }, @@ -114,11 +122,11 @@ const places = [ Marker styles - Draw markers in HTML with .map-marker: a dot, a circle with an icon or a count, a pin, or an avatar. + Draw markers in HTML with .map-marker: a dot, a circle with an icon or a count, a pin, or an avatar. Add .map-marker-label for a short text.
-
+
Store
12
@@ -180,7 +188,7 @@ const places = [ type: 'line', source: 'route', layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': tablerMapColor('--tblr-primary'), 'line-width': 4 }, + paint: { 'line-color': tablerMapColor('--tblr-primary'), 'line-width': 3, 'line-opacity': 0.8 }, }) }) }) @@ -220,7 +228,7 @@ const places = [ Clusters Group many points into clusters that split as you zoom in. Click a cluster to zoom to it. - + @@ -353,12 +365,79 @@ const places = [
-
+
+ + + Globe + Show the map as a 3D globe with setProjection, and connect places with arcs. + +
+ + {hub.name} + +
+ { + destinations.map((place) => ( +
+ + {place.name} + +
+ )) + } +
+ + + +
+
+
+ +
Draggable marker Add draggable: true to let people move a marker, for example to pick an address. - +
diff --git a/shared/components/demo/Map.astro b/shared/components/demo/Map.astro index 056cb5202..87a5c6306 100644 --- a/shared/components/demo/Map.astro +++ b/shared/components/demo/Map.astro @@ -17,15 +17,17 @@ interface Props { title?: string; style?: string; controls?: boolean; + /** draws the map as a 3D globe instead of a flat map */ + globe?: boolean; } -const { mapId, ratio, center: centerProp, zoom: zoomProp, title, style: styleProp, controls: controlsProp } = Astro.props; +const { mapId, ratio, center: centerProp, zoom: zoomProp, title, style: styleProp, controls: controlsProp, globe = false } = Astro.props; type MapData = { title?: string | undefined; ratio?: string; card?: boolean; - /** OpenFreeMap style name: liberty (default), bright, positron, dark or fiord */ + /** OpenFreeMap style name: positron (default), liberty, bright, dark or fiord */ style?: string | undefined; /** style used in dark mode, `dark` by default */ darkStyle?: string; @@ -41,7 +43,7 @@ const hasHtmlMarkers = Astro.slots.has('default'); const mapContainerId = `map-${mapId}`; const styleUrl = (name: string) => `https://tiles.openfreemap.org/styles/${name}`; -const style = styleUrl(data?.style ?? 'liberty'); +const style = styleUrl(data?.style ?? 'positron'); const darkStyle = styleUrl(data?.darkStyle ?? 'dark'); const controls = data?.controls ?? false; const zoom = data?.zoom ?? 13; @@ -55,7 +57,8 @@ const ratioClass = data?.ratio ?? ratio ?? '16x9'; data && (
- ) @@ -71,7 +74,7 @@ const ratioClass = data?.ratio ?? ratio ?? '16x9'; data && ( -