diff --git a/.changeset/maps-maplibre-styles.md b/.changeset/maps-maplibre-styles.md new file mode 100644 index 000000000..90d182bb6 --- /dev/null +++ b/.changeset/maps-maplibre-styles.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/preview": patch +"@tabler/docs": patch +--- + +Added MapLibre GL styles to `tabler-vendors.css`: map controls, popups and attribution, in light and dark mode. diff --git a/core/package.json b/core/package.json index 555bdbf1d..e5d718d4f 100644 --- a/core/package.json +++ b/core/package.json @@ -196,19 +196,19 @@ }, { "path": "./dist/css/tabler-vendors.css", - "maxSize": "4 kB" + "maxSize": "4.5 kB" }, { "path": "./dist/css/tabler-vendors.min.css", - "maxSize": "3.5 kB" - }, - { - "path": "./dist/css/tabler-vendors.rtl.css", "maxSize": "4 kB" }, + { + "path": "./dist/css/tabler-vendors.rtl.css", + "maxSize": "4.5 kB" + }, { "path": "./dist/css/tabler-vendors.rtl.min.css", - "maxSize": "3.5 kB" + "maxSize": "4 kB" }, { "path": "./dist/js/tabler.js", diff --git a/core/scss/tabler-vendors.scss b/core/scss/tabler-vendors.scss index cce656e31..8633982ba 100644 --- a/core/scss/tabler-vendors.scss +++ b/core/scss/tabler-vendors.scss @@ -4,6 +4,7 @@ @forward 'vendor/tom-select'; @forward 'vendor/apexcharts'; @forward 'vendor/jsvectormap'; +@forward 'vendor/maplibre'; @forward 'vendor/dropzone'; @forward 'vendor/fslightbox'; @forward 'vendor/plyr'; 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 cc24d90a3..88e717e1b 100644 --- a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap +++ b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap @@ -90,6 +90,7 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre "--tblr-secondary", "--tblr-secondary-bg", "--tblr-shadow-card", + "--tblr-shadow-color", "--tblr-shadow-dropdown", "--tblr-shadow-input", "--tblr-signal-color", diff --git a/core/scss/vendor/_maplibre.scss b/core/scss/vendor/_maplibre.scss new file mode 100644 index 000000000..562d9fc08 --- /dev/null +++ b/core/scss/vendor/_maplibre.scss @@ -0,0 +1,133 @@ +@use '../config' as *; + +.maplibregl-map { + font-family: inherit; + font-size: $h5-font-size; + line-height: $line-height-base; +} + +// Controls: zoom, compass, fullscreen, locate +.maplibregl-ctrl-group { + overflow: hidden; + background: var(--bg-surface); + @include border-radius(var(--border-radius)); + + &:not(:empty) { + box-shadow: + 0 0 0 var(--border-width) var(--border-color), + #{$card-box-shadow}; + } + + button { + width: 2rem; + height: 2rem; + + + button { + border-top: var(--border-width) var(--border-style) var(--border-color); + } + + &:focus:focus-visible { + outline-offset: calc(-1 * var(--focus-ring-width)); + box-shadow: none; + @include focus-ring(); + } + } +} + +.maplibregl-ctrl button:not(:disabled):hover, +.maplibregl-ctrl button:not(:disabled):active { + background-color: var(--bg-surface-secondary); +} + +// Attribution +.maplibregl-ctrl.maplibregl-ctrl-attrib { + color: var(--secondary); + background-color: color-transparent(var(--bg-surface), 0.7); + + a { + color: inherit; + } +} + +.maplibregl-ctrl-attrib.maplibregl-compact { + color: var(--secondary); + background-color: var(--bg-surface); +} + +.maplibregl-ctrl-attrib-button:focus { + box-shadow: none; + @include focus-ring(); +} + +.maplibregl-ctrl-scale { + color: var(--body-color); + background-color: color-transparent(var(--bg-surface), 0.75); + border-color: var(--body-color); +} + +// Popup +.maplibregl-popup-content { + padding: 0.75rem 1rem; + color: var(--body-color); + background: var(--bg-surface); + box-shadow: + 0 0 0 var(--border-width) var(--border-color), + #{$box-shadow-md}; + @include border-radius(var(--border-radius)); +} + +// Room for the close button, which MapLibre pins to the right edge in both directions +.maplibregl-popup-content:has(.maplibregl-popup-close-button) { + /* rtl:ignore */ + padding-right: 2.25rem; +} + +.maplibregl-popup-close-button { + width: 1.5rem; + height: 1.5rem; + padding: 0; + margin: 0.375rem; + font-size: 1rem; + line-height: 1; + color: var(--secondary); + @include border-radius(var(--border-radius)); + + &:hover { + color: var(--body-color); + background-color: var(--bg-surface-secondary); + } +} + +.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); +} + +// The control icons are black SVG images +@if $enable-dark-mode { + @include color-mode(dark) { + .maplibregl-ctrl-icon, + .maplibregl-ctrl-attrib-button { + @include autodark-image(); + } + } +} diff --git a/docs/content/ui/components/map.mdx b/docs/content/ui/components/map.mdx index 69714cae6..be3b2fa86 100644 --- a/docs/content/ui/components/map.mdx +++ b/docs/content/ui/components/map.mdx @@ -40,6 +40,13 @@ Or include it from a CDN. You need two files: the stylesheet and the library. `} /> +Tabler styles the map controls, popups and attribution to match the interface, in light and dark mode. These styles are in `tabler-vendors.css`, so load it after the MapLibre stylesheet: + +`} +/> + ## Usage ### Markup @@ -115,6 +122,53 @@ 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. +### 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. + +```js +new maplibregl.Marker({ color: 'var(--tblr-primary)' }) + .setLngLat([16.366667, 48.2]) + .setPopup(new maplibregl.Popup({ offset: 32 }).setText('Vienna')) + .addTo(map) +``` + +Use `setHTML` instead of `setText` for richer content. Only pass HTML you trust, as it is not escaped. + +### Controls + +Add the zoom and compass buttons with `NavigationControl`. They go to the top right corner by default. + +```js +map.addControl(new maplibregl.NavigationControl()) +``` + +MapLibre also has `FullscreenControl`, `GeolocateControl` and `ScaleControl`. Pass a corner as the second argument, for example `'bottom-left'`. + +### Dark mode + +A map style does not change with the page theme on its own. Pick the style from the `data-bs-theme` attribute, and set it again when the attribute changes: + +```js +const themeStyle = function () { + const dark = document.documentElement.getAttribute('data-bs-theme') === 'dark' + return 'https://tiles.openfreemap.org/styles/' + (dark ? 'dark' : 'liberty') +} + +const map = new maplibregl.Map({ + container: 'map-simple', + style: themeStyle(), + zoom: 13, + center: [13.4049, 52.518827], +}) + +new MutationObserver(function () { + map.setStyle(themeStyle()) +}).observe(document.documentElement, { attributeFilter: ['data-bs-theme'] }) +``` + +Markers and popups stay on the map when the style changes. The maps on this page follow the theme this way. + ### Attribution MapLibre adds the credit for the map data in the corner of the map. OpenFreeMap requires it, so do not hide it. diff --git a/preview/pages/map-fullsize.astro b/preview/pages/map-fullsize.astro index f1b00c114..bc2ee098f 100644 --- a/preview/pages/map-fullsize.astro +++ b/preview/pages/map-fullsize.astro @@ -12,15 +12,20 @@ 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 map = new maplibregl.Map({ container: 'map-fullsize', - style: 'https://tiles.openfreemap.org/styles/liberty', + style: themeStyle(), zoom: 12, center: [13.4049, 52.518827], }) map.addControl(new maplibregl.NavigationControl()) + // Change the map style when the page theme changes + new MutationObserver(() => map.setStyle(themeStyle())).observe(document.documentElement, { attributeFilter: ['data-bs-theme'] }) + window.tabler_map['map-fullsize'] = map } diff --git a/shared/components/demo/Map.astro b/shared/components/demo/Map.astro index 4363946ab..1637130bf 100644 --- a/shared/components/demo/Map.astro +++ b/shared/components/demo/Map.astro @@ -1,5 +1,6 @@ --- // MapLibre GL map on OpenFreeMap tiles: no account and no access token. +// The map follows the page theme: `style` in light mode, `darkStyle` in dark. // Renders nothing unless maps[mapId] exists. import maps from '@data/maps.json'; import CaptureScript from '../CaptureScript.astro'; @@ -18,6 +19,10 @@ type MapData = { card?: boolean; /** OpenFreeMap style name: liberty (default), bright, positron, dark or fiord */ style?: string; + /** style used in dark mode, `dark` by default */ + darkStyle?: string; + /** adds the zoom and compass buttons */ + controls?: boolean; zoom?: number; center?: [number, number]; markers?: { name?: string; center: [number, number] }[]; @@ -26,10 +31,13 @@ type MapData = { const data = (maps as unknown as Record)[mapId]; const mapContainerId = `map-${mapId}`; -const style = `https://tiles.openfreemap.org/styles/${data?.style ?? 'liberty'}`; +const styleUrl = (name: string) => `https://tiles.openfreemap.org/styles/${name}`; +const style = styleUrl(data?.style ?? 'liberty'); +const darkStyle = styleUrl(data?.darkStyle ?? 'dark'); +const controls = data?.controls ?? false; const zoom = data?.zoom ?? 13; const center = data?.center ? [data.center[1], data.center[0]] : [13.4049, 52.518827]; -const markers = (data?.markers ?? []).map((marker) => [marker.center[1], marker.center[0]]); +const markers = (data?.markers ?? []).map((marker) => ({ name: marker.name, coords: [marker.center[1], marker.center[0]] })); const ratioClass = data?.ratio ?? ratio ?? '16x9'; --- @@ -47,21 +55,33 @@ const ratioClass = data?.ratio ?? ratio ?? '16x9'; data && ( -