45 Commits

Author SHA1 Message Date
Alexis
3acfc6db47 Merge branch 'dev' 2025-06-22 14:29:08 +02:00
Alexis
6c350bf2d5 Removed new item for mine map 2025-06-22 14:28:26 +02:00
Alexis
712d408736 Added theme switcher 2025-06-21 21:33:25 +02:00
Alexis
bfdd433a5f Added dark mode ui for leaflet controls 2025-06-21 14:32:23 +02:00
Alexis
ac6558392c Fixed some more UI with dark mode 2025-06-21 11:49:15 +02:00
Alexis
484fe3b031 Fixed search results in dark mode 2025-06-21 11:33:20 +02:00
Alexis
c88133a9a3 Moved some variables to root 2025-06-20 20:06:07 +02:00
Alexis
1cb77dd6f0 Version bump 2025-06-20 17:20:42 +02:00
Alexis
a2fe47210f Updated packages 2025-06-20 17:20:29 +02:00
Alexis
38dfd3c952 Updated readme 2025-06-20 17:11:59 +02:00
Alexis
19100d0881 Hotfix : Spelling issues on legal notice fr 2025-06-15 21:47:09 +02:00
Alexis
e8ac66b2bd Merge branch 'dev' 2025-06-14 19:13:22 +02:00
Alexis
464cf334cf Updated packages and added artwork notice 2025-06-14 19:11:33 +02:00
Alexis
dd8a506ef3 Added legal notice modal 2025-06-14 18:57:13 +02:00
Alexis
a2253a165f Added legal notice link 2025-06-06 18:28:00 +02:00
Alexis
dbfce2a737 Updated packages 2025-06-06 16:34:29 +02:00
Alexis
b660c61b81 Updated packages 2025-06-02 16:47:09 +02:00
Alexis
78a16ba907 Version bump 2025-05-24 12:39:25 +02:00
Alexis
99e66f6feb Merge branch 'dev' 2025-05-24 12:38:58 +02:00
Alexis
cfe3fe2dcc Updated packages 2025-05-24 12:38:50 +02:00
Alexis
42b3cfd725 Added bad bots list 2025-05-24 12:36:28 +02:00
Alexis
d3c0a87770 Version bump 2025-05-13 10:58:09 +02:00
Alexis
3e1d7c5200 Updated packages 2025-05-13 10:57:48 +02:00
Alexis
e893e133cc Added toast color 2025-05-04 20:39:34 +02:00
Alexis
4cf2f66d42 Added copy coords function 2025-05-04 18:33:42 +02:00
Alexis
f003df7d58 Added toast service 2025-05-03 21:48:32 +02:00
Alexis
355e9ae009 Merge branch 'dev' 2025-05-03 14:25:43 +02:00
Alexis
ca689c23af Merge branch 'cleanup' into dev 2025-05-03 14:25:05 +02:00
Alexis
c282c7ec7a Added env variables to disable some options 2025-05-02 18:54:28 +02:00
Alexis
0bc65e96c8 Updated package 2025-05-02 18:00:41 +02:00
Alexis
c6bf77462e Version bump 2025-04-10 19:21:46 +02:00
Alexis
fb26813932 Merge branch 'cleanup' 2025-04-08 16:50:47 +02:00
Alexis
6152b707d1 Minimize zoomify leaflet js 2025-04-05 18:45:05 +02:00
Alexis
a40bf13d98 Persist coords on lang switch 2025-04-04 22:08:09 +02:00
Alexis
c68b1baf8c Added docs for interfaces 2025-04-04 21:46:56 +02:00
Alexis
199bb98412 Marker tooltip content is a component 2025-04-04 21:13:08 +02:00
Alexis
82aa2f4d35 Updated package 2025-04-04 17:13:25 +02:00
Alexis
86dbe7ba2b Hover enter more lenient 2025-04-03 16:47:33 +02:00
Alexis
8b976e909a Fixed some accessibility issues 2025-04-02 14:02:53 +02:00
Alexis
47f5df936e Fixed meta semicolon 2025-04-02 12:17:40 +02:00
Alexis
6d281e14ea Added prefect to astro links 2025-04-02 12:17:12 +02:00
Alexis
cd99ed2444 Updated packages 2025-04-02 12:03:18 +02:00
Alexis
3e1382c404 Changed images to webp 2025-03-27 23:08:14 +01:00
Alexis
c84745b35a Version bump 2025-03-27 22:41:16 +01:00
Alexis
f5a76f9c6e Updated packages 2025-03-27 22:41:03 +01:00
91 changed files with 2480 additions and 1332 deletions

View File

@@ -1,5 +1,15 @@
# Leim Maps # Leim Maps
Repository for interactive maps using Leim's Wiki data 🗺️ A simple Astro static site to display custom maps with Leaflet. Includes sample images with maps from my own world.
## Demo ## Features
Live demo available at https://maps.alexcreates.fr * Add your own markers for cities, NPCs, or quests
* Search through markers
* Create multiple maps for different continents or countries
* Measure distances between two points
* Custom client-side markers
## Contributing
I'm not currently accepting pull-requests for this project, but if you have a suggestion, feel free to contact me !
## Notes
As someone with some degree of with [aphantasia](https://en.wikipedia.org/wiki/Aphantasia), I struggle with visualization and immersion, and DMing in general. If you find yourself in the same situation, I hope my little thing made your life easier !

View File

@@ -1,21 +1,21 @@
import { defineConfig } from 'astro/config'; import { defineConfig, envField } from 'astro/config';
import { availableLangs, defaultLang } from './src/i18n/ui';
import vue from "@astrojs/vue"; import vue from "@astrojs/vue";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [vue(), sitemap()], integrations: [vue(), sitemap()],
output: 'static', output: 'static',
site: 'https://maps.alexcreates.fr', site: 'https://maps.alexcreates.fr',
// i18n: { prefetch: true,
// locales: availableLangs, env: {
// defaultLocale: defaultLang, schema: {
// routing: { ENABLE_TOOLBAR: envField.boolean({ context: "server", access: "secret", default: true }),
// prefixDefaultLocale: true, ENABLE_TOOLBAR_TAGS: envField.boolean({ context: "server", access: "secret", default: true }),
// } ENABLE_CUSTOM_MARKERS: envField.boolean({ context: "server", access: "secret", default: true }),
// } ENABLE_BREADCRUMB: envField.boolean({ context: "server", access: "secret", default: true }),
ENABLE_I18N: envField.boolean({ context: "server", access: "secret", default: true }),
}
}
}); });

View File

@@ -1,7 +1,7 @@
{ {
"name": "leim-maps", "name": "leim-maps",
"type": "module", "type": "module",
"version": "1.4.6", "version": "1.4.11",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "start": "astro dev",
@@ -10,18 +10,18 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/sitemap": "^3.3.0", "@astrojs/sitemap": "^3.4.1",
"@astrojs/vue": "^5.0.7", "@astrojs/vue": "^5.1.0",
"@nanostores/persistent": "^0.10.2", "@nanostores/persistent": "1.0.0",
"@nanostores/vue": "^0.11.0", "@nanostores/vue": "^1.0.0",
"@types/leaflet": "^1.9.17", "@types/leaflet": "^1.9.18",
"@vueuse/core": "^13.0.0", "@vueuse/core": "^13.4.0",
"astro": "^5.5.4", "astro": "^5.10.0",
"nanostores": "^0.11.4", "nanostores": "^1.0.1",
"radix-vue": "^1.9.17", "radix-vue": "^1.9.17",
"vue": "^3.5.13" "vue": "^3.5.17"
}, },
"devDependencies": { "devDependencies": {
"sass": "^1.86.0" "sass": "^1.89.2"
} }
} }

1902
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,5 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild
- sharp
- vue-demi

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="#FFF"><path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#FFF" viewBox="0 0 256 256"><path d="M235.32,73.37,182.63,20.69a16,16,0,0,0-22.63,0L20.68,160a16,16,0,0,0,0,22.63l52.69,52.68a16,16,0,0,0,22.63,0L235.32,96A16,16,0,0,0,235.32,73.37ZM84.68,224,32,171.31l32-32,26.34,26.35a8,8,0,0,0,11.32-11.32L75.31,128,96,107.31l26.34,26.35a8,8,0,0,0,11.32-11.32L107.31,96,128,75.31l26.34,26.35a8,8,0,0,0,11.32-11.32L139.31,64l32-32L224,84.69Z"></path></svg>

After

Width:  |  Height:  |  Size: 461 B

1
public/icon/ruler.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#020617" viewBox="0 0 256 256"><path d="M235.32,73.37,182.63,20.69a16,16,0,0,0-22.63,0L20.68,160a16,16,0,0,0,0,22.63l52.69,52.68a16,16,0,0,0,22.63,0L235.32,96A16,16,0,0,0,235.32,73.37ZM84.68,224,32,171.31l32-32,26.34,26.35a8,8,0,0,0,11.32-11.32L75.31,128,96,107.31l26.34,26.35a8,8,0,0,0,11.32-11.32L107.31,96,128,75.31l26.34,26.35a8,8,0,0,0,11.32-11.32L139.31,64l32-32L224,84.69Z"></path></svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M224,104a8,8,0,0,1-13.66,5.66L184,83.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L172.69,72,146.34,45.66A8,8,0,0,1,152,32h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z" fill="#FFF" /></svg>

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,130 +1,4 @@
/* L.TileLayer.Zoomify=L.TileLayer.extend({options:{width:-1,height:-1,tileGroupPrefix:'TileGroup',tilesPerTileGroup:256},initialize:function(url,options){L.TileLayer.prototype.initialize.call(this,url,options);if(this.options.width<0||this.options.height<0){throw new Error('The user must set the Width and Height of the Zoomify image')}},beforeAdd:function(map){var imageSize=L.point(this.options.width,this.options.height);this._imageSize=[imageSize];this._gridSize=[this._getGridSize(imageSize)];while(imageSize.x>this.options.tileSize||imageSize.y>this.options.tileSize){imageSize=imageSize.divideBy(2).ceil();this._imageSize.push(imageSize);this._gridSize.push(this._getGridSize(imageSize))}
* L.TileLayer.Zoomify display Zoomify tiles with Leaflet this._imageSize.reverse();this._gridSize.reverse();var maxNativeZoom=this._gridSize.length-1;this.options.maxNativeZoom=maxNativeZoom;var maxZoomGrid=this._gridSize[maxNativeZoom],maxX=maxZoomGrid.x*this.options.tileSize,maxY=maxZoomGrid.y*this.options.tileSize,southEast=map.unproject([maxX,maxY],maxNativeZoom);this.options.bounds=new L.LatLngBounds([[0,0],southEast]);L.TileLayer.prototype.beforeAdd.call(this,map)},_getGridSize:function(imageSize){var tileSize=this.options.tileSize;return L.point(Math.ceil(imageSize.x/tileSize),Math.ceil(imageSize.y/tileSize))},_addTile:function(coords,container){L.TileLayer.prototype._addTile.call(this,coords,container);var imageSize=this._imageSize[this._getZoomForUrl()],gridSize=this._gridSize[this._getZoomForUrl()];var realTileSize=L.GridLayer.prototype.getTileSize.call(this),displayTileSize=L.TileLayer.prototype.getTileSize.call(this);var key=this._tileCoordsToKey(coords),tile=this._tiles[key].el;var scaleFactor=L.point((imageSize.x%realTileSize.x),(imageSize.y%realTileSize.y)).unscaleBy(realTileSize);if((imageSize.x%realTileSize.x)>0&&coords.x===gridSize.x-1){tile.style.width=displayTileSize.scaleBy(scaleFactor).x+'px'}
* if((imageSize.y%realTileSize.y)>0&&coords.y===gridSize.y-1){tile.style.height=displayTileSize.scaleBy(scaleFactor).y+'px'}},getTileUrl:function(coords){this.options.g=this.options.tileGroupPrefix+this._getTileGroup(coords);return L.TileLayer.prototype.getTileUrl.call(this,coords)},_getTileGroup:function(coords){var zoom=this._getZoomForUrl(),num=0,gridSize;for(var z=0;z<zoom;z++){gridSize=this._gridSize[z];num+=gridSize.x*gridSize.y}
* Based on the Leaflet.Zoomify (https://github.com/turban/Leaflet.Zoomify) num+=coords.y*this._gridSize[zoom].x+coords.x;return Math.floor(num/this.options.tilesPerTileGroup)},getBounds:function(){return this.options.bounds}});L.tileLayer.zoomify=function(url,options){return new L.TileLayer.Zoomify(url,options)}
* from turban (https://github.com/turban)
*
*/
L.TileLayer.Zoomify = L.TileLayer.extend({
options: {
width: -1, // Must be set by user, max zoom image width
height: -1, // Must be set by user, max zoom image height
tileGroupPrefix: 'TileGroup',
tilesPerTileGroup: 256
},
initialize: function (url, options) {
L.TileLayer.prototype.initialize.call(this, url, options);
// Replace with automatic loading from ImageProperties.xml
if (this.options.width < 0 || this.options.height < 0) {
throw new Error('The user must set the Width and Height of the Zoomify image');
}
},
beforeAdd: function (map) {
var imageSize = L.point(this.options.width, this.options.height);
// Build the zoom sizes of the pyramid and cache them in an array
this._imageSize = [imageSize];
this._gridSize = [this._getGridSize(imageSize)];
// Register the image size in pixels and the grid size in # of tiles for each zoom level
while (imageSize.x > this.options.tileSize || imageSize.y > this.options.tileSize) {
imageSize = imageSize.divideBy(2).ceil();
this._imageSize.push(imageSize);
this._gridSize.push(this._getGridSize(imageSize));
}
// We built the cache from bottom to top, but leaflet uses a top to bottom index for the zoomlevel,
// so reverse it for easy indexing by current zoomlevel
this._imageSize.reverse();
this._gridSize.reverse();
// Register our max supported zoom level
var maxNativeZoom = this._gridSize.length - 1;
this.options.maxNativeZoom = maxNativeZoom;
// Register our bounds for this zoomify layer based on the maximum zoom
var maxZoomGrid = this._gridSize[maxNativeZoom],
maxX = maxZoomGrid.x * this.options.tileSize,
maxY = maxZoomGrid.y * this.options.tileSize,
southEast = map.unproject([maxX, maxY], maxNativeZoom);
this.options.bounds = new L.LatLngBounds([[0, 0], southEast]);
L.TileLayer.prototype.beforeAdd.call(this, map);
},
// Calculate the grid size for a given image size (based on tile size)
_getGridSize: function (imageSize) {
var tileSize = this.options.tileSize;
return L.point(Math.ceil(imageSize.x / tileSize), Math.ceil(imageSize.y / tileSize));
},
// Extend the add tile function to update our arbitrary sized border tiles
_addTile: function (coords, container) {
// Load the tile via the original leaflet code
L.TileLayer.prototype._addTile.call(this, coords, container);
// Get out imagesize in pixels for this zoom level and our grid size
var imageSize = this._imageSize[this._getZoomForUrl()],
gridSize = this._gridSize[this._getZoomForUrl()];
// The real tile size (default:256) and the display tile size (if zoom > maxNativeZoom)
var realTileSize = L.GridLayer.prototype.getTileSize.call(this),
displayTileSize = L.TileLayer.prototype.getTileSize.call(this);
// Get the current tile to adjust
var key = this._tileCoordsToKey(coords),
tile = this._tiles[key].el;
// Calculate the required size of the border tiles
var scaleFactor = L.point( (imageSize.x % realTileSize.x),
(imageSize.y % realTileSize.y)).unscaleBy(realTileSize);
// Update tile dimensions if we are on a border
if ((imageSize.x % realTileSize.x) > 0 && coords.x === gridSize.x - 1) {
tile.style.width = displayTileSize.scaleBy(scaleFactor).x + 'px';
}
if ((imageSize.y % realTileSize.y) > 0 && coords.y === gridSize.y - 1) {
tile.style.height = displayTileSize.scaleBy(scaleFactor).y + 'px';
}
},
// Construct the tile url, by inserting our tilegroup before we template the url
getTileUrl: function (coords) {
// Set our TileGroup variable, and then let the original tile templater do the work
this.options.g = this.options.tileGroupPrefix + this._getTileGroup(coords);
// Call the original templater
return L.TileLayer.prototype.getTileUrl.call(this, coords);
},
// Calculates the TileGroup number, each group contains 256 tiles. The tiles are stored from topleft to bottomright
_getTileGroup: function (coords) {
var zoom = this._getZoomForUrl(),
num = 0,
gridSize;
// Get the total number of tiles from the lowest zoom level to our zoomlevel
for (var z = 0; z < zoom; z++) {
gridSize = this._gridSize[z];
num += gridSize.x * gridSize.y;
}
// Add the remaining tiles from this zoom layer to the running total of tiles
num += coords.y * this._gridSize[zoom].x + coords.x;
return Math.floor(num / this.options.tilesPerTileGroup);
},
getBounds: function () {
return this.options.bounds;
}
});
L.tileLayer.zoomify = function (url, options) {
return new L.TileLayer.Zoomify(url, options);
};

49
public/robots.txt Normal file
View File

@@ -0,0 +1,49 @@
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
User-agent: Google-Extended
Disallow: /
User-agent: PerplexityBot
Disallow: /
User-agent: Amazonbot
Disallow: /
User-agent: ClaudeBot
Disallow: /
User-agent: Omgilibot
Disallow: /
User-Agent: FacebookBot
Disallow: /
User-Agent: Applebot
Disallow: /
User-agent: anthropic-ai
Disallow: /
User-agent: Bytespider
Disallow: /
User-agent: Claude-Web
Disallow: /
User-agent: Diffbot
Disallow: /
User-agent: ImagesiftBot
Disallow: /
User-agent: Omgilibot
Disallow: /
User-agent: Omgili
Disallow: /
User-agent: YouBot
Disallow: /
User-agent: AhrefsBot
Disallow: /
User-agent: AhrefsSiteAudit
Disallow: /
User-agent: SemrushBot
Disallow: /
User-agent: YandexMarket
Disallow: /
User-agent: YandexBot
Disallow: /
User-agent: DataForSeoBot
Disallow: /
Sitemap: https://maps.alexcreates.fr/sitemap-index.xml

View File

@@ -1,7 +1,7 @@
// Animations on load // Animations on load
@media screen and (prefers-reduced-motion: no-preference) { @media screen and (prefers-reduced-motion: no-preference) {
.appear-from-top { .appear-from-top {
animation: fadeIn .2s cubic-bezier(0.785, 0.135, 0.15, 0.86); animation: fadeIn .2s var(--timing-function);
} }
@keyframes fadeIn { @keyframes fadeIn {

View File

@@ -5,12 +5,13 @@
gap: .5rem; gap: .5rem;
font-size: .9em; font-size: .9em;
font-weight: 500; font-weight: 500;
color: var(--foreground);
border: 1px solid transparent; border: 1px solid transparent;
border-radius: .2rem; border-radius: var(--border-radius);
cursor: pointer; cursor: pointer;
transition-property: color, background-color, border-color; transition-property: color, background-color, border-color;
transition-duration: .2s; transition-duration: .2s;
transition-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86); transition-timing-function: var(--timing-function);
&:disabled { &:disabled {
cursor: not-allowed; cursor: not-allowed;
@@ -18,40 +19,40 @@
&.btn-secondary { &.btn-secondary {
&:not(:disabled) { &:not(:disabled) {
background-color: var(--slate-200); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
&:hover, &:hover,
&:focus-visible { &:focus-visible {
color: var(--white); color: var(--background);
background-color: var(--blue-500); background-color: var(--accent);
} }
} }
&:disabled { &:disabled {
color: var(--slate-500); color: color-mix(in srgb, var(--foreground) 50%, var(--background));
background-color: var(--slate-100); background-color: color-mix(in srgb, var(--foreground) 10%, var(--background));
} }
} }
&.btn-info { &.btn-info {
&:not(:disabled) { &:not(:disabled) {
background-color: var(--slate-200); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
&:hover, &:hover,
&:focus-visible { &:focus-visible {
color: var(--white); color: var(--background);
background-color: var(--blue-500); background-color: var(--accent);
} }
} }
} }
&.btn-danger { &.btn-danger {
&:not(:disabled) { &:not(:disabled) {
background-color: var(--slate-200); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
&:hover, &:hover,
&:focus-visible { &:focus-visible {
color: var(--white); color: var(--background);
background-color: var(--red-500); background-color: var(--red-500);
} }
} }
@@ -76,7 +77,7 @@
} }
&.btn-shadow { &.btn-shadow {
box-shadow: rgba(0, 0, 0, 0.15) 0 .2rem .3rem; box-shadow: var(--shadow);
} }
} }
@@ -85,41 +86,42 @@
place-items: center; place-items: center;
height: 45px; height: 45px;
aspect-ratio: 1; aspect-ratio: 1;
background-color: var(--white); background-color: var(--background);
border: 1px solid var(--slate-400); border: 1px solid var(--border);
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; box-shadow: var(--shadow);
border-radius: 100vmax; border-radius: 100vmax;
pointer-events: all; pointer-events: all;
cursor: pointer; cursor: pointer;
outline: .2rem solid transparent; outline: .2rem solid transparent;
transition-property: color, background-color, border-color, outline-color; transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s; transition-duration: .15s;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95); transition-timing-function: var(--timing-function);
svg { svg {
fill: var(--foreground);
width: 1.15em; width: 1.15em;
transition-property: fill; transition-property: fill;
transition-duration: .15s; transition-duration: .15s;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95); transition-timing-function: var(--timing-function);
} }
&:hover, &:hover,
&:focus-visible { &:focus-visible {
outline-color: color-mix(in srgb, var(--blue-500) 20%, transparent); outline-color: color-mix(in srgb, var(--accent) 20%, transparent);
border-color: var(--blue-500); border-color: var(--accent);
svg { svg {
fill: var(--blue-500); fill: var(--accent);
} }
} }
&.active { &.active {
color: var(--white); color: var(--background);
background-color: var(--blue-500); background-color: var(--accent);
border-color: var(--blue-700); border-color: color-mix(in srgb, var(--accent) 75%, var(--background));
svg { svg {
fill: var(--white); fill: var(--background);
} }
} }
} }

View File

@@ -2,13 +2,14 @@
padding-block: 0.75rem; padding-block: 0.75rem;
padding-inline: .75rem; padding-inline: .75rem;
border-radius: 8px; border-radius: 8px;
background-color: var(--white); color: var(--foreground);
border: 1px solid var(--slate-100); background-color: var(--background);
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; border: 1px solid var(--border);
box-shadow: var(--shadow);
min-width: 15rem; min-width: 15rem;
.card-arrow { .card-arrow {
fill: var(--white); fill: var(--background);
} }
&.p-sm { &.p-sm {

View File

@@ -14,15 +14,15 @@
display: block; display: block;
width: 100%; width: 100%;
padding: .4rem .5rem; padding: .4rem .5rem;
border: 1px solid var(--slate-300); border: 1px solid color-mix(in srgb, var(--foreground) 30%, var(--background));
font-size: .8em; font-size: .8em;
border-radius: .2rem; border-radius: var(--border-radius);
transition-property: box-shadow; transition-property: box-shadow;
transition-duration: .2s; transition-duration: .2s;
transition-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86); transition-timing-function: var(--timing-function);
&:focus-visible { &:focus-visible {
box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--slate-300) 20%, transparent); box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--accent) 20%, transparent);
} }
} }
} }

View File

@@ -10,148 +10,40 @@
.leaflet-map-pane { .leaflet-map-pane {
.leaflet-popup-pane { .leaflet-popup-pane {
.leaflet-popup-content-wrapper { .leaflet-popup-content-wrapper {
border-radius: .25rem; color: var(--foreground);
background-color: var(--background);
border-radius: var(--border-radius);
overflow: clip; overflow: clip;
.leaflet-popup-content { .leaflet-popup-content {
min-width: 20rem; min-width: 20rem;
margin: 1rem; margin: 1rem;
figure {
position: relative;
margin-inline: -1.1rem;
margin-top: -1.1rem;
margin-bottom: .5rem;
&.landscape {
aspect-ratio: 16 / 9;
min-width: 20rem;
}
&.portrait {
aspect-ratio: 15 / 16;
min-width: 10rem;
}
&::before {
content: '';
display: block;
pointer-events: none;
user-select: none;
position: absolute;
inset: 0;
z-index: 10;
// Make a linear gradient from top to bottom, from transparent to black
background: linear-gradient(to bottom, transparent 70%, var(--black) 100%);
}
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
}
figcaption cite {
position: absolute;
font-size: 85%;
font-style: italic;
font-weight: 600;
bottom: .5rem;
right: 1rem;
color: var(--white);
z-index: 20;
opacity: .6;
transition-property: opacity;
transition-duration: .2s;
transition-timing-function: cubic-bezier(0.23, 1, 0.320, 1);
a {
padding-right: 0;
color: var(--white);
text-decoration: underline;
text-underline-offset: .15rem;
&:hover {
color: var(--blue-500);
}
&::after {
display: none;
}
}
}
&:hover {
figcaption cite {
opacity: 1;
}
}
}
.title {
display: block;
font-weight: 600;
}
strong.title {
color: var(--green-500);
}
a.title,
a.map-link {
text-underline-offset: 2px;
text-decoration: underline;
&:hover,
&:focus-visible {
text-decoration: none;
color: var(--green-500);
}
}
.map-link {
display: inline-flex;
align-items: center;
gap: .5ch;
svg {
width: 1rem;
height: 1rem;
}
}
.tooltip-content > * + * {
margin: 0;
margin-top: .66em;
}
.tooltip-footer {
display: grid;
gap: .15em;
font-size: .95em;
}
} }
} }
.leaflet-popup-tip { .leaflet-popup-tip {
width: .8rem; width: .8rem;
height: .8rem; height: .8rem;
background-color: var(--background);
} }
} }
.leaflet-tooltip-pane { .leaflet-tooltip-pane {
.leaflet-tooltip { .leaflet-tooltip {
&.capital-name { &.capital-name {
padding: .2rem .4rem; padding: .2rem .4rem;
color: #EEE; color: var(--foreground);
font-weight: bold; font-weight: bold;
background: none; background: none;
border: none; border: none;
box-shadow: none; box-shadow: none;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
transition-property: color, text-shadow;
transition-duration: .15s;
transition-timing-function: var(--timing-function);
@include text.stroke(2, #18181b); @include text.stroke(2, var(--background));
&::before { &::before {
display: none; display: none;
@@ -161,3 +53,56 @@
} }
} }
} }
.leaflet-touch .leaflet-bar {
border-color: var(--border);
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
color: var(--foreground);
background-color: var(--background);
border-color: var(--border);
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: var(--timing-function);
&:hover,
&:focus-visible {
color: var(--background);
background-color: var(--accent);
}
}
}
.leaflet-touch .leaflet-control-measure {
color: var(--foreground);
background-color: var(--background);
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: var(--timing-function);
.leaflet-control-measure-toggle {
color: var(--foreground);
background-color: var(--background);
border-color: var(--border);
background-image: url('/icon/ruler.svg');
background-size: 66%;
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: var(--timing-function);
&:hover,
&:focus-visible {
color: var(--background);
background-color: var(--accent);
}
}
}
.dark {
.leaflet-touch .leaflet-control-measure {
.leaflet-control-measure-toggle {
background-image: url('/icon/ruler-dark.svg');
}
}
}

View File

@@ -0,0 +1,88 @@
.dialog-overlay {
background-color: color-mix(in srgb, var(--black) 50%, transparent);
position: fixed;
inset: 0;
animation: show-overlay 150ms var(--timing-function);
z-index: 100;
}
.dialog-content {
position: fixed;
top: 50%;
left: 50%;
z-index: 200;
transform: translate(-50%, -50%);
width: 50rem;
max-width: calc(100% - 4rem);
max-height: 70vh;
overflow-y: auto;
padding: 25px;
display: grid;
grid-template-rows: auto 1fr;
color: var(--foreground);
border-radius: var(--border-radius);
background-color: var(--background);
border: 1px solid var(--border);
box-shadow: var(--shadow);
animation: show-content 150ms var(--timing-function);
&:focus {
outline: none;
}
}
.dialog-title {
margin-bottom: .5em;
font-size: 1.5em;
font-weight: 700;
}
.dialog-description {
overflow-y: auto;
padding-right: 1rem;
h3 {
font-size: 1.25em;
font-weight: 700;
}
font-size: 1em;
color: color-mix(in srgb, var(--foreground) 80%, var(--background));
line-height: 1.5;
> * + * {
margin-top: 1em;
}
a {
position: relative;
text-underline-offset: 2px;
text-decoration: underline;
&:hover,
&:focus-visible {
text-decoration: none;
color: var(--green-500);
}
}
}
@keyframes show-overlay {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes show-content {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}

View File

@@ -0,0 +1,115 @@
.toast-viewport {
--viewport-padding: 25px;
position: fixed;
bottom: 0;
right: 2rem;
display: flex;
flex-direction: column;
padding: var(--viewport-padding);
gap: 10px;
width: 480px;
max-width: 100vw;
margin: 0;
list-style: none;
z-index: 2147483647;
outline: none;
}
.toast-root {
position: relative;
padding: 15px;
display: grid;
grid-template-areas: 'title action' 'description action';
grid-template-columns: auto max-content;
column-gap: 15px;
align-items: center;
color: var(--foreground);
border: 1px solid var(--border);
background-color: var(--background);
border-radius: var(--border-radius);
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
&.toast-success {
color: var(--green-700);
border: 1px solid var(--green-700);
background-color: color-mix(in srgb, var(--green-500) 17.5%, var(--background));
.toast-icon svg {
fill: var(--green-700);
}
}
&:has(.toast-icon) {
padding-left: 2.2rem;
}
.toast-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: .66rem;
}
}
.toast-root[data-state='open'] {
animation: slideIn 150ms var(--timing-function);
}
.toast-root[data-state='closed'] {
animation: hide 100ms var(--timing-function);
}
.toast-root[data-swipe='move'] {
transform: translateX(var(--radix-toast-swipe-move-x));
}
.toast-root[data-swipe='cancel'] {
transform: translateX(0);
transition: transform 200ms var(--timing-function);
}
.toast-root[data-swipe='end'] {
animation: swipeOut 100ms var(--timing-function);
}
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideIn {
from {
transform: translateX(calc(100% + var(--viewport-padding)));
}
to {
transform: translateX(0);
}
}
@keyframes swipeOut {
from {
transform: translateX(var(--radix-toast-swipe-end-x));
}
to {
transform: translateX(calc(100% + var(--viewport-padding)));
}
}
.toast-title {
grid-area: title;
margin-bottom: 5px;
font-weight: 500;
color: var(--slate-12);
font-size: 15px;
}
.toast-description {
grid-area: description;
margin: 0;
color: var(--slate-11);
font-size: 13px;
line-height: 1.3;
}
.toast-action {
grid-area: action;
}

View File

@@ -9,6 +9,9 @@
@use 'card'; @use 'card';
@use 'map'; @use 'map';
@use 'radix/toast';
@use 'radix/dialog';
:root { :root {
--white: #fff; --white: #fff;
--black: #111; --black: #111;
@@ -34,19 +37,34 @@
--orange-700: #c2410c; --orange-700: #c2410c;
--purple-500: #8b5cf6; --purple-500: #8b5cf6;
--purple-700: #6d28d9; --purple-700: #6d28d9;
--foreground: var(--black);
--background: var(--white);
--border: var(--slate-400);
--accent: var(--blue-500);
--border-radius: .25rem;
--shadow: color-mix(in srgb, var(--foreground) 10%, transparent) 0px 4px 12px;
--timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
}
.dark {
--foreground: var(--slate-300);
--background: var(--slate-950);
--border: var(--slate-600);
} }
html { html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
color: var(--slate-900); color: var(--foreground);
} }
a[href^='http'] { a[href^='http'] {
padding-right: 1.25em; white-space: nowrap;
orphans: 2;
} }
a[href^='http']::after { a[href^='http']::after {
position: absolute;
content: url('/icon/square-arrow-out.svg'); content: url('/icon/square-arrow-out.svg');
display: inline-block; display: inline-block;
width: 1em; width: 1em;
@@ -54,6 +72,16 @@ a[href^='http']::after {
margin-left: .25em; margin-left: .25em;
} }
.dark {
a[href^='http']::after {
content: url('/icon/square-arrow-out-dark.svg');
}
}
strong {
font-weight: 600;
}
.icon { .icon {
width: 1em; width: 1em;
} }

View File

@@ -62,7 +62,7 @@ button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: .5ch; gap: .5ch;
color: var(--slate-700); color: color-mix(in srgb, var(--foreground) 70%, var(--background));
cursor: pointer; cursor: pointer;
text-underline-offset: 2px; text-underline-offset: 2px;
text-decoration: none; text-decoration: none;
@@ -75,7 +75,7 @@ button {
svg { svg {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
fill: var(--slate-700); fill: color-mix(in srgb, var(--foreground) 70%, var(--background));
} }
} }
</style> </style>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark({ storageKey: "theme", disableTransition: false })
const toggleDark = useToggle(isDark)
</script>
<template>
<div data-theme-toggle id="theme-switcher" class="wrapper appear-from-top">
<button @click="toggleDark()" class="btn-round">
<svg v-if="isDark" xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M120,40V32a8,8,0,0,1,16,0v8a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-8-8A8,8,0,0,0,50.34,61.66Zm0,116.68-8,8a8,8,0,0,0,11.32,11.32l8-8a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l8-8a8,8,0,0,0-11.32-11.32l-8,8A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l8,8a8,8,0,0,0,11.32-11.32ZM40,120H32a8,8,0,0,0,0,16h8a8,8,0,0,0,0-16Zm88,88a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,128,208Zm96-88h-8a8,8,0,0,0,0,16h8a8,8,0,0,0,0-16Z"></path></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56,104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"></path></svg>
</button>
</div>
</template>
<style lang="scss" scoped>
.wrapper {
pointer-events: all;
}
.btn-round {
position: relative;
> svg {
width: 1.25em;
height: 1.25em;
}
}
</style>

View File

@@ -1,11 +1,10 @@
--- ---
import { ENABLE_CUSTOM_MARKERS } from "astro:env/server";
import type { MapMarker, PlayerMarker } from '@/types/Leaflet'; import type { MapMarker, PlayerMarker } from '@/types/Leaflet';
import type { MapProps } from '@/types/Map'; import type { MapProps } from '@/types/Map';
import MarkerCreator from './overlay/MarkerCreator.vue'; import MarkerCreator from './overlay/MarkerCreator.vue';
import { t } from '@/i18n/store'; import MarkerContent from './MarkerContent.astro';
import { getLangFromUrl } from '@/i18n/utils';
import { normalizeString } from '@/utils/Strings';
import CopyText from '../global/CopyText.astro';
interface Props extends MapProps {} interface Props extends MapProps {}
@@ -21,55 +20,21 @@ const {
rulerHideWalkDistance = false, rulerHideWalkDistance = false,
backgroundColor = 'transparent', backgroundColor = 'transparent',
} = Astro.props } = Astro.props
const lang = getLangFromUrl(Astro.url);
const byText = t('common.by', lang)
const seeMapText = t('maps.go-to-map', lang)
--- ---
<div class="map-wrapper"> <div class="map-wrapper">
<div class="world-wrapper"> <main class="world-wrapper">
<div id="world"></div> <div id="world"></div>
<MarkerCreator client:only="vue" mapKey={mapKey} /> {ENABLE_CUSTOM_MARKERS &&
</div> <MarkerCreator client:only="vue" mapKey={mapKey} />
}
</main>
<div style="height: 0; width: 0; overflow: hidden; display: none;"> <div style="height: 0; width: 0; overflow: hidden; display: none;">
{markers.map((m, index) => ( {markers.map((m, index) => (
<div id="popup-template" data-id={`marker-${index}`} class="tooltip-content"> <div id="popup-template" data-id={`marker-${index}`}>
{m.cover && m.coverAuthor && m.coverLink && ( <MarkerContent marker={m} />
<figure class={m.coverPortrait ? 'portrait' : 'landscape'}>
<img src={`/images/cover/${m.cover}`} alt={m.title} width={m.coverPortrait ? 210 : 420} />
<figcaption>
<cite>{byText} <a href={m.coverLink} target="_blank">{m.coverAuthor}</a></cite>
</figcaption>
</figure>
)}
{m.link ? (
<a href={m.link} target="_blank" class="title">{m.title}</a>
) : (
<strong class="title">{m.title}</strong>
)}
{m.description && <p>{m.description}</p>}
<div class="tooltip-footer">
{m.mapId && (
<div>
<a href={m.mapId} class="map-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="80" y1="112" x2="144" y2="112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="112" cy="112" r="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="168.57" y1="168.57" x2="224" y2="224" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="80" x2="112" y2="144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
<span>
{ seeMapText }
</span>
</a>
</div>
)}
<div>
<CopyText text={`${Astro.url}?p=${normalizeString(m.title)}`} />
</div>
</div>
</div> </div>
))} ))}
</div> </div>
@@ -85,9 +50,7 @@ const seeMapText = t('maps.go-to-map', lang)
mapWidth, mapWidth,
rulerDistanceRatio, rulerDistanceRatio,
rulerMainUnit, rulerMainUnit,
rulerHideWalkDistance, rulerHideWalkDistance
byText,
seeMapText
}} }}
defer defer
> >
@@ -639,6 +602,19 @@ map.on('moveend', () => {
setupToURL() setupToURL()
}) })
/**
* Listener for querying a copy of the current coordinates
*/
window.addEventListener('on-copy-coords', () => {
const lon = convertScaleToX(Number(localStorage.getItem('lastHeldXPosition')))
const lat = convertScaleToY(Number(localStorage.getItem('lastHeldYPosition')))
const coordsText = JSON.stringify({ x: lon, y: lat })
navigator.clipboard.writeText(coordsText)
window.dispatchEvent(new CustomEvent('on-toast-push', { bubbles: true, detail: { preset: 'copied-coords' }}))
})
/** /**
* UTILITIES * UTILITIES
*/ */
@@ -646,7 +622,7 @@ map.on('moveend', () => {
* Convert x coords to scale with ratio, zoom and offset * Convert x coords to scale with ratio, zoom and offset
* @param {number} x * @param {number} x
*/ */
function convertXToScale(x) { function convertXToScale(x) {
return (x * xRatio * zoomRatio) + xOffset return (x * xRatio * zoomRatio) + xOffset
} }
/** /**

View File

@@ -1,10 +1,14 @@
--- ---
import { ENABLE_BREADCRUMB, ENABLE_I18N, ENABLE_TOOLBAR, ENABLE_TOOLBAR_TAGS } from "astro:env/server";
import type { MapMarker, PlayerMarker } from "@/types/Leaflet"; import type { MapMarker, PlayerMarker } from "@/types/Leaflet";
import type { MapOverlayProps } from "@/types/Map"; import type { MapOverlayProps } from "@/types/Map";
import SearchMarkers from "./overlay/SearchMarkers.vue"; import SearchMarkers from "./overlay/SearchMarkers.vue";
import LangSwitcher from "./overlay/LangSwitcher.vue"; import LangSwitcher from "./overlay/LangSwitcher.vue";
import MapOverlayBreadcrumbs from "./MapOverlayBreadcrumbs.astro"; import MapOverlayBreadcrumbs from "./MapOverlayBreadcrumbs.astro";
import ToastService from "./overlay/ToastService.vue";
import ThemeSwitch from "../global/ThemeSwitch.vue";
interface Props extends MapOverlayProps {} interface Props extends MapOverlayProps {}
@@ -25,16 +29,36 @@ const currentUrl = Astro.url
<div class="world-overlay"> <div class="world-overlay">
<div class="top-bar"> <div class="top-bar">
<SearchMarkers client:only="vue" mapKey={mapKey} markers={markers} players={players} searchConfig={searchConfig} > {ENABLE_TOOLBAR &&
<div slot="fallback" style="height: 45px;"></div> <SearchMarkers
</SearchMarkers> client:only="vue"
mapKey={mapKey}
markers={markers}
players={players}
searchConfig={searchConfig}
disableTags={!ENABLE_TOOLBAR_TAGS}
>
<div slot="fallback" style="height: 45px;"></div>
</SearchMarkers>
}
<LangSwitcher currentUrl={currentUrl} client:only="vue" />
<div class="right-menu">
{ENABLE_I18N &&
<LangSwitcher currentUrl={currentUrl} client:only="vue" />
}
<ThemeSwitch client:only="vue" />
</div>
</div> </div>
<MapOverlayBreadcrumbs breadcrumbs={breadcrumbs} /> {ENABLE_BREADCRUMB &&
<MapOverlayBreadcrumbs breadcrumbs={breadcrumbs} />
}
</div> </div>
<ToastService client:only="vue" />
<style lang="scss"> <style lang="scss">
.world-overlay { .world-overlay {
position: absolute; position: absolute;
@@ -53,5 +77,15 @@ const currentUrl = Astro.url
align-items: top; align-items: top;
gap: 1rem; gap: 1rem;
} }
.right-menu {
display: grid;
gap: .5rem;
align-content: flex-start;
@media screen and (width >= 900px) {
display: flex;
}
}
} }
</style> </style>

View File

@@ -14,24 +14,24 @@ const { breadcrumbs = [] } = Astro.props
{breadcrumbs.map((breadcrumb, index) => ( {breadcrumbs.map((breadcrumb, index) => (
<li> <li>
{index === breadcrumbs.length - 1 && {index === breadcrumbs.length - 1 &&
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="folder"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64,123.2,86.4A8,8,0,0,0,128,88h72v16H69.77a16,16,0,0,0-15.18,10.94L40,158.7V64Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="icon folder"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64,123.2,86.4A8,8,0,0,0,128,88h72v16H69.77a16,16,0,0,0-15.18,10.94L40,158.7V64Z"></path></svg>
<span class="breadcrumb" aria-current="page"> <span class="breadcrumb" aria-current="page">
<span class="label"> <h1 class="label">
{breadcrumb.name} {breadcrumb.name}
</span> </h1>
</span> </span>
} }
{!(index === breadcrumbs.length - 1) && {!(index === breadcrumbs.length - 1) &&
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="folder"><path d="M216,72H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.62A15.4,15.4,0,0,0,39.38,216H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72ZM40,56H92.69l16,16H40ZM216,200H40V88H216Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0f172a" viewBox="0 0 256 256" class="icon folder"><path d="M216,72H131.31L104,44.69A15.86,15.86,0,0,0,92.69,40H40A16,16,0,0,0,24,56V200.62A15.4,15.4,0,0,0,39.38,216H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72ZM40,56H92.69l16,16H40ZM216,200H40V88H216Z"></path></svg>
<a href={breadcrumb.url} class="breadcrumb" class:list={{ active: index === breadcrumbs.length - 1 }}> <a href={breadcrumb.url} class="breadcrumb" class:list={{ active: index === breadcrumbs.length - 1 }} data-astro-prefetch="hover">
<span class="label"> <span class="label">
{breadcrumb.name} {breadcrumb.name}
</span> </span>
</a> </a>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="#0f172a" viewBox="0 0 256 256"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="#0f172a" viewBox="0 0 256 256" class="icon"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path></svg>
} }
</li> </li>
))} ))}
@@ -48,17 +48,25 @@ menu {
margin-top: .5rem; margin-top: .5rem;
padding-block: .25rem; padding-block: .25rem;
padding-inline: .8rem; padding-inline: .8rem;
border: 1px solid var(--slate-400); color: var(--foreground);
background: var(--white); border: 1px solid var(--border);
background: var(--background);
border-radius: 100vmax; border-radius: 100vmax;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
pointer-events: all; pointer-events: all;
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: var(--timing-function);
li { li {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .25ch; gap: .25ch;
svg {
fill: var(--foreground);
}
&:has(a:hover, a:focus-visible) { &:has(a:hover, a:focus-visible) {
svg.folder { svg.folder {
fill: var(--blue-500); fill: var(--blue-500);
@@ -83,7 +91,7 @@ menu {
} }
.icon { .icon {
width: 1.1em; fill: var(--foreground);
} }
.label { .label {

View File

@@ -0,0 +1,176 @@
---
import type { MapMarker } from '@/types/Leaflet';
import { normalizeString } from '@/utils/Strings';
import { getLangFromUrl } from '@/i18n/utils';
import { t } from '@/i18n/store';
import CopyText from '@/components/global/CopyText.astro';
interface Props {
marker: MapMarker
}
const { marker: m } = Astro.props as Props;
const lang = getLangFromUrl(Astro.url);
const byText = t('common.by', lang)
const seeMapText = t('maps.go-to-map', lang)
---
<div class="tooltip-content">
{m.cover && m.coverAuthor && m.coverLink && (
<figure class={m.coverPortrait ? 'portrait' : 'landscape'}>
<img src={`/images/cover/${m.cover}`} alt={m.title} width={m.coverPortrait ? 210 : 420} />
<figcaption>
<span class="author">
<span>{byText}</span>
<cite>
<a href={m.coverLink} target="_blank">{m.coverAuthor}</a>
</cite>
</span>
</figcaption>
</figure>
)}
{m.link ? (
<a href={m.link} target="_blank" class="title">{m.title}</a>
) : (
<strong class="title">{m.title}</strong>
)}
{m.description && <p set:html={m.description} />}
<div class="tooltip-footer">
{m.mapId && (
<div>
<a href={m.mapId} class="map-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="80" y1="112" x2="144" y2="112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="112" cy="112" r="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="168.57" y1="168.57" x2="224" y2="224" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="112" y1="80" x2="112" y2="144" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
<span>
{ seeMapText }
</span>
</a>
</div>
)}
<div>
<CopyText text={`${Astro.url}?p=${normalizeString(m.title)}`} />
</div>
</div>
</div>
<style lang="scss">
figure {
position: relative;
margin-inline: -1.1rem;
margin-top: -1.1rem;
margin-bottom: .5rem;
&.landscape {
aspect-ratio: 16 / 9;
min-width: 20rem;
}
&.portrait {
aspect-ratio: 15 / 16;
min-width: 10rem;
}
&::before {
content: '';
display: block;
pointer-events: none;
user-select: none;
position: absolute;
inset: 0;
z-index: 10;
// Make a linear gradient from top to bottom, from transparent to black
background: linear-gradient(to bottom, transparent 70%, var(--black) 100%);
}
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: top;
}
figcaption .author {
position: absolute;
font-size: 85%;
font-style: italic;
font-weight: 600;
bottom: .5rem;
right: 1rem;
color: var(--white);
z-index: 20;
opacity: .6;
transition-property: opacity;
transition-duration: .2s;
transition-timing-function: cubic-bezier(0.23, 1, 0.320, 1);
a {
padding-right: 0;
color: var(--white);
text-decoration: underline;
text-underline-offset: .15rem;
&:hover {
color: var(--blue-500);
}
&::after {
display: none;
}
}
}
&:hover {
figcaption .author {
opacity: 1;
}
}
}
.title {
display: block;
font-weight: 600;
}
strong.title {
color: var(--green-500);
}
a.title,
a.map-link {
text-underline-offset: 2px;
text-decoration: underline;
&:hover,
&:focus-visible {
text-decoration: none;
color: var(--green-500);
}
}
.map-link {
display: inline-flex;
align-items: center;
gap: .5ch;
svg {
width: 1rem;
height: 1rem;
}
}
.tooltip-content > * + * {
margin: 0;
margin-top: .66em;
}
.tooltip-footer {
display: grid;
gap: .15em;
font-size: .95em;
}
</style>

View File

@@ -9,7 +9,7 @@
"x": 83.6, "x": 83.6,
"y": -185.2 "y": -185.2
}, },
"cover": "Ambrose.jpg", "cover": "Ambrose.webp",
"coverAuthor": "Claudio Pilia", "coverAuthor": "Claudio Pilia",
"coverLink": "https://pilia-art.com/portfolio-item/game-of-thrones-the-city-of-tyrosh/" "coverLink": "https://pilia-art.com/portfolio-item/game-of-thrones-the-city-of-tyrosh/"
}, },
@@ -22,7 +22,7 @@
"x": 133.46875, "x": 133.46875,
"y": -146.0625 "y": -146.0625
}, },
"cover": "Cordouac.jpg", "cover": "Cordouac.webp",
"coverAuthor": "Vincent Joyal", "coverAuthor": "Vincent Joyal",
"coverLink": "https://www.artstation.com/artwork/6B0b0" "coverLink": "https://www.artstation.com/artwork/6B0b0"
}, },
@@ -45,7 +45,7 @@
"x": 132.26953, "x": 132.26953,
"y": -98.19141 "y": -98.19141
}, },
"cover": "Tourgrise.png", "cover": "Tourgrise.webp",
"coverAuthor": "saya kuroha", "coverAuthor": "saya kuroha",
"coverLink": "https://www.pixiv.net/en/artworks/83793821", "coverLink": "https://www.pixiv.net/en/artworks/83793821",
"coverPortrait": true "coverPortrait": true
@@ -79,7 +79,7 @@
"x": 168.39491, "x": 168.39491,
"y": -109.13376 "y": -109.13376
}, },
"cover": "Port-brisé.jpg", "cover": "Port-brisé.webp",
"coverAuthor": "JC Jongwon Park", "coverAuthor": "JC Jongwon Park",
"coverLink": "https://www.artstation.com/artwork/okmQL" "coverLink": "https://www.artstation.com/artwork/okmQL"
}, },
@@ -122,7 +122,7 @@
"x": 204.42515, "x": 204.42515,
"y": -226.85883 "y": -226.85883
}, },
"cover": "Soubreciel.jpg", "cover": "Soubreciel.webp",
"coverAuthor": "Tony Andreas Rudolph", "coverAuthor": "Tony Andreas Rudolph",
"coverLink": "https://www.artstation.com/artwork/EB84" "coverLink": "https://www.artstation.com/artwork/EB84"
}, },
@@ -166,7 +166,7 @@
"y": -51.54687 "y": -51.54687
}, },
"mapId": "/en/aldys/borelis", "mapId": "/en/aldys/borelis",
"cover": "Port_de_Borélis.jpeg", "cover": "Port_de_Borélis.webp",
"coverAuthor": "Anton Bezrukov", "coverAuthor": "Anton Bezrukov",
"coverLink": "https://www.artstation.com/artwork/oODL5B" "coverLink": "https://www.artstation.com/artwork/oODL5B"
}, },
@@ -210,7 +210,7 @@
"y": -210.87224307207197 "y": -210.87224307207197
}, },
"mapId": "/en/aldys/cantane", "mapId": "/en/aldys/cantane",
"cover": "Cantane.jpg", "cover": "Cantane.webp",
"coverAuthor": "Andreas Roller", "coverAuthor": "Andreas Roller",
"coverLink": "https://en.wikipedia.org/wiki/Andreas_Roller" "coverLink": "https://en.wikipedia.org/wiki/Andreas_Roller"
}, },
@@ -223,7 +223,7 @@
"y": -210.9293951755887 "y": -210.9293951755887
}, },
"mapId": "/en/aldys/cantane/mines-blanches", "mapId": "/en/aldys/cantane/mines-blanches",
"cover": "Les Mines Blanches.png", "cover": "Les Mines Blanches.webp",
"coverAuthor": "MeckanicalMind", "coverAuthor": "MeckanicalMind",
"coverLink": "https://www.deviantart.com/meckanicalmind/art/R3-Mines-258469134" "coverLink": "https://www.deviantart.com/meckanicalmind/art/R3-Mines-258469134"
}, },
@@ -236,7 +236,7 @@
"x": 185.57431, "x": 185.57431,
"y": -229.08588 "y": -229.08588
}, },
"cover": "Horfer.jpg", "cover": "Horfer.webp",
"coverAuthor": "Nele-Diel", "coverAuthor": "Nele-Diel",
"coverLink": "https://www.deviantart.com/nele-diel/art/City-in-the-Lowlands-693529688" "coverLink": "https://www.deviantart.com/nele-diel/art/City-in-the-Lowlands-693529688"
}, },
@@ -519,7 +519,7 @@
"x": 121.47116769539711, "x": 121.47116769539711,
"y": -94.66997667550434 "y": -94.66997667550434
}, },
"cover": "Faille_de_Kiam.jpg", "cover": "Faille_de_Kiam.webp",
"coverAuthor": "Laura Sava", "coverAuthor": "Laura Sava",
"coverLink": "https://www.behance.net/gallery/16130223/Zariels-Doom", "coverLink": "https://www.behance.net/gallery/16130223/Zariels-Doom",
"coverPortrait": true "coverPortrait": true
@@ -573,7 +573,7 @@
"x": 107.50352, "x": 107.50352,
"y": -67.36631 "y": -67.36631
}, },
"cover": "Willeau.jpg", "cover": "Willeau.webp",
"coverAuthor": "Inconnu", "coverAuthor": "Inconnu",
"coverLink": "https://www.hebus.com/image-339033.html" "coverLink": "https://www.hebus.com/image-339033.html"
}, },
@@ -635,7 +635,7 @@
"x": 168.31641, "x": 168.31641,
"y": -60.54297 "y": -60.54297
}, },
"cover": "Tours_des_Cages.jpg", "cover": "Tours_des_Cages.webp",
"coverAuthor": "25kartinok", "coverAuthor": "25kartinok",
"coverLink": "https://www.deviantart.com/25kartinok/art/the-dark-tower-700231527", "coverLink": "https://www.deviantart.com/25kartinok/art/the-dark-tower-700231527",
"coverPortrait": true "coverPortrait": true
@@ -677,7 +677,7 @@
"x": 159.57688930332125, "x": 159.57688930332125,
"y": -157.4739168466964 "y": -157.4739168466964
}, },
"cover": "Plaines_de_poussière.png", "cover": "Plaines_de_poussière.webp",
"coverAuthor": "Inconnu", "coverAuthor": "Inconnu",
"coverLink": "https://www.pinterest.com/pin/pin-de-soup-em-inspiration--279223245638830461" "coverLink": "https://www.pinterest.com/pin/pin-de-soup-em-inspiration--279223245638830461"
}, },
@@ -710,7 +710,7 @@
"x": 285.0990986858211, "x": 285.0990986858211,
"y": -146.08986069534564 "y": -146.08986069534564
}, },
"cover": "Garràn.jpg", "cover": "Garràn.webp",
"coverAuthor": "Dmitry Dubinsky", "coverAuthor": "Dmitry Dubinsky",
"coverLink": "https://elhour.artstation.com/projects/RYynYm?album_id=47640" "coverLink": "https://elhour.artstation.com/projects/RYynYm?album_id=47640"
}, },
@@ -773,7 +773,7 @@
"x": 326.7497076703842, "x": 326.7497076703842,
"y": -206.67114645854178 "y": -206.67114645854178
}, },
"cover": "Fasàch.jpg", "cover": "Fasàch.webp",
"coverAuthor": "Liang Mark", "coverAuthor": "Liang Mark",
"coverLink": "https://www.artstation.com/artwork/Jll2Jd" "coverLink": "https://www.artstation.com/artwork/Jll2Jd"
}, },
@@ -886,7 +886,7 @@
"x": 83.80737652386473, "x": 83.80737652386473,
"y": -184.52963682119173 "y": -184.52963682119173
}, },
"cover": "Académie_Centrale_Kaldélienne.jpg", "cover": "Académie_Centrale_Kaldélienne.webp",
"coverAuthor": "Pengzhen Zhang", "coverAuthor": "Pengzhen Zhang",
"coverLink": "https://www.artstation.com/artwork/zAOWNd", "coverLink": "https://www.artstation.com/artwork/zAOWNd",
"icon": "graduation-cap" "icon": "graduation-cap"

View File

@@ -9,7 +9,7 @@
"x": 119.6667886200924, "x": 119.6667886200924,
"y": -17.945469721492717 "y": -17.945469721492717
}, },
"cover": "Faranne.jpg", "cover": "Faranne.webp",
"coverAuthor": "Pablo Mendoza Peces", "coverAuthor": "Pablo Mendoza Peces",
"coverLink": "https://www.artstation.com/artwork/dAJr1" "coverLink": "https://www.artstation.com/artwork/dAJr1"
}, },
@@ -32,7 +32,7 @@
"x": 113.52851847725508, "x": 113.52851847725508,
"y": -19.882939646724424 "y": -19.882939646724424
}, },
"cover": "Miverne.jpg", "cover": "Miverne.webp",
"coverAuthor": "Andrei-Pervukhin", "coverAuthor": "Andrei-Pervukhin",
"coverLink": "https://www.deviantart.com/andrei-pervukhin/art/Medieval-Village-383779337" "coverLink": "https://www.deviantart.com/andrei-pervukhin/art/Medieval-Village-383779337"
}, },
@@ -75,7 +75,7 @@
"x": 122.87040892704968, "x": 122.87040892704968,
"y": -58.7910005354329 "y": -58.7910005354329
}, },
"cover": "Rocheverre.jpg", "cover": "Rocheverre.webp",
"coverAuthor": "Creative Intelligence Arts", "coverAuthor": "Creative Intelligence Arts",
"coverLink": "https://www.kickstarter.com/projects/1300298569/project-phoenix-japans-indie-rpg-feat-aaa-talent" "coverLink": "https://www.kickstarter.com/projects/1300298569/project-phoenix-japans-indie-rpg-feat-aaa-talent"
}, },

View File

@@ -9,7 +9,7 @@
"x": 83.6, "x": 83.6,
"y": -185.2 "y": -185.2
}, },
"cover": "Ambrose.jpg", "cover": "Ambrose.webp",
"coverAuthor": "Claudio Pilia", "coverAuthor": "Claudio Pilia",
"coverLink": "https://pilia-art.com/portfolio-item/game-of-thrones-the-city-of-tyrosh/" "coverLink": "https://pilia-art.com/portfolio-item/game-of-thrones-the-city-of-tyrosh/"
}, },
@@ -22,7 +22,7 @@
"x": 133.46875, "x": 133.46875,
"y": -146.0625 "y": -146.0625
}, },
"cover": "Cordouac.jpg", "cover": "Cordouac.webp",
"coverAuthor": "Vincent Joyal", "coverAuthor": "Vincent Joyal",
"coverLink": "https://www.artstation.com/artwork/6B0b0" "coverLink": "https://www.artstation.com/artwork/6B0b0"
}, },
@@ -45,7 +45,7 @@
"x": 132.26953, "x": 132.26953,
"y": -98.19141 "y": -98.19141
}, },
"cover": "Tourgrise.png", "cover": "Tourgrise.webp",
"coverAuthor": "saya kuroha", "coverAuthor": "saya kuroha",
"coverLink": "https://www.pixiv.net/en/artworks/83793821", "coverLink": "https://www.pixiv.net/en/artworks/83793821",
"coverPortrait": true "coverPortrait": true
@@ -79,7 +79,7 @@
"x": 168.39491, "x": 168.39491,
"y": -109.13376 "y": -109.13376
}, },
"cover": "Port-brisé.jpg", "cover": "Port-brisé.webp",
"coverAuthor": "JC Jongwon Park", "coverAuthor": "JC Jongwon Park",
"coverLink": "https://www.artstation.com/artwork/okmQL" "coverLink": "https://www.artstation.com/artwork/okmQL"
}, },
@@ -122,7 +122,7 @@
"x": 204.42515, "x": 204.42515,
"y": -226.85883 "y": -226.85883
}, },
"cover": "Soubreciel.jpg", "cover": "Soubreciel.webp",
"coverAuthor": "Tony Andreas Rudolph", "coverAuthor": "Tony Andreas Rudolph",
"coverLink": "https://www.artstation.com/artwork/EB84" "coverLink": "https://www.artstation.com/artwork/EB84"
}, },
@@ -166,7 +166,7 @@
"y": -51.54687 "y": -51.54687
}, },
"mapId": "/fr/aldys/borelis", "mapId": "/fr/aldys/borelis",
"cover": "Port_de_Borélis.jpeg", "cover": "Port_de_Borélis.webp",
"coverAuthor": "Anton Bezrukov", "coverAuthor": "Anton Bezrukov",
"coverLink": "https://www.artstation.com/artwork/oODL5B" "coverLink": "https://www.artstation.com/artwork/oODL5B"
}, },
@@ -210,7 +210,7 @@
"y": -210.87224307207197 "y": -210.87224307207197
}, },
"mapId": "/fr/aldys/cantane", "mapId": "/fr/aldys/cantane",
"cover": "Cantane.jpg", "cover": "Cantane.webp",
"coverAuthor": "Andreas Roller", "coverAuthor": "Andreas Roller",
"coverLink": "https://en.wikipedia.org/wiki/Andreas_Roller" "coverLink": "https://en.wikipedia.org/wiki/Andreas_Roller"
}, },
@@ -223,7 +223,7 @@
"y": -210.9293951755887 "y": -210.9293951755887
}, },
"mapId": "/fr/aldys/cantane/mines-blanches", "mapId": "/fr/aldys/cantane/mines-blanches",
"cover": "Les Mines Blanches.png", "cover": "Les Mines Blanches.webp",
"coverAuthor": "MeckanicalMind", "coverAuthor": "MeckanicalMind",
"coverLink": "https://www.deviantart.com/meckanicalmind/art/R3-Mines-258469134" "coverLink": "https://www.deviantart.com/meckanicalmind/art/R3-Mines-258469134"
}, },
@@ -236,7 +236,7 @@
"x": 185.57431, "x": 185.57431,
"y": -229.08588 "y": -229.08588
}, },
"cover": "Horfer.jpg", "cover": "Horfer.webp",
"coverAuthor": "Nele-Diel", "coverAuthor": "Nele-Diel",
"coverLink": "https://www.deviantart.com/nele-diel/art/City-in-the-Lowlands-693529688" "coverLink": "https://www.deviantart.com/nele-diel/art/City-in-the-Lowlands-693529688"
}, },
@@ -519,7 +519,7 @@
"x": 121.47116769539711, "x": 121.47116769539711,
"y": -94.66997667550434 "y": -94.66997667550434
}, },
"cover": "Faille_de_Kiam.jpg", "cover": "Faille_de_Kiam.webp",
"coverAuthor": "Laura Sava", "coverAuthor": "Laura Sava",
"coverLink": "https://www.behance.net/gallery/16130223/Zariels-Doom", "coverLink": "https://www.behance.net/gallery/16130223/Zariels-Doom",
"coverPortrait": true "coverPortrait": true
@@ -573,7 +573,7 @@
"x": 107.50352, "x": 107.50352,
"y": -67.36631 "y": -67.36631
}, },
"cover": "Willeau.jpg", "cover": "Willeau.webp",
"coverAuthor": "Inconnu", "coverAuthor": "Inconnu",
"coverLink": "https://www.hebus.com/image-339033.html" "coverLink": "https://www.hebus.com/image-339033.html"
}, },
@@ -635,7 +635,7 @@
"x": 168.31641, "x": 168.31641,
"y": -60.54297 "y": -60.54297
}, },
"cover": "Tours_des_Cages.jpg", "cover": "Tours_des_Cages.webp",
"coverAuthor": "25kartinok", "coverAuthor": "25kartinok",
"coverLink": "https://www.deviantart.com/25kartinok/art/the-dark-tower-700231527", "coverLink": "https://www.deviantart.com/25kartinok/art/the-dark-tower-700231527",
"coverPortrait": true "coverPortrait": true
@@ -677,7 +677,7 @@
"x": 159.57688930332125, "x": 159.57688930332125,
"y": -157.4739168466964 "y": -157.4739168466964
}, },
"cover": "Plaines_de_poussière.png", "cover": "Plaines_de_poussière.webp",
"coverAuthor": "Inconnu", "coverAuthor": "Inconnu",
"coverLink": "https://www.pinterest.com/pin/pin-de-soup-em-inspiration--279223245638830461" "coverLink": "https://www.pinterest.com/pin/pin-de-soup-em-inspiration--279223245638830461"
}, },
@@ -710,7 +710,7 @@
"x": 285.0990986858211, "x": 285.0990986858211,
"y": -146.08986069534564 "y": -146.08986069534564
}, },
"cover": "Garràn.jpg", "cover": "Garràn.webp",
"coverAuthor": "Dmitry Dubinsky", "coverAuthor": "Dmitry Dubinsky",
"coverLink": "https://elhour.artstation.com/projects/RYynYm?album_id=47640" "coverLink": "https://elhour.artstation.com/projects/RYynYm?album_id=47640"
}, },
@@ -773,7 +773,7 @@
"x": 326.7497076703842, "x": 326.7497076703842,
"y": -206.67114645854178 "y": -206.67114645854178
}, },
"cover": "Fasàch.jpg", "cover": "Fasàch.webp",
"coverAuthor": "Liang Mark", "coverAuthor": "Liang Mark",
"coverLink": "https://www.artstation.com/artwork/Jll2Jd" "coverLink": "https://www.artstation.com/artwork/Jll2Jd"
}, },
@@ -886,7 +886,7 @@
"x": 83.80737652386473, "x": 83.80737652386473,
"y": -184.52963682119173 "y": -184.52963682119173
}, },
"cover": "Académie_Centrale_Kaldélienne.jpg", "cover": "Académie_Centrale_Kaldélienne.webp",
"coverAuthor": "Pengzhen Zhang", "coverAuthor": "Pengzhen Zhang",
"coverLink": "https://www.artstation.com/artwork/zAOWNd", "coverLink": "https://www.artstation.com/artwork/zAOWNd",
"icon": "graduation-cap" "icon": "graduation-cap"

View File

@@ -9,7 +9,7 @@
"x": 119.6667886200924, "x": 119.6667886200924,
"y": -17.945469721492717 "y": -17.945469721492717
}, },
"cover": "Faranne.jpg", "cover": "Faranne.webp",
"coverAuthor": "Pablo Mendoza Peces", "coverAuthor": "Pablo Mendoza Peces",
"coverLink": "https://www.artstation.com/artwork/dAJr1" "coverLink": "https://www.artstation.com/artwork/dAJr1"
}, },
@@ -32,7 +32,7 @@
"x": 113.52851847725508, "x": 113.52851847725508,
"y": -19.882939646724424 "y": -19.882939646724424
}, },
"cover": "Miverne.jpg", "cover": "Miverne.webp",
"coverAuthor": "Andrei-Pervukhin", "coverAuthor": "Andrei-Pervukhin",
"coverLink": "https://www.deviantart.com/andrei-pervukhin/art/Medieval-Village-383779337" "coverLink": "https://www.deviantart.com/andrei-pervukhin/art/Medieval-Village-383779337"
}, },
@@ -75,7 +75,7 @@
"x": 122.87040892704968, "x": 122.87040892704968,
"y": -58.7910005354329 "y": -58.7910005354329
}, },
"cover": "Rocheverre.jpg", "cover": "Rocheverre.webp",
"coverAuthor": "Creative Intelligence Arts", "coverAuthor": "Creative Intelligence Arts",
"coverLink": "https://www.kickstarter.com/projects/1300298569/project-phoenix-japans-indie-rpg-feat-aaa-talent" "coverLink": "https://www.kickstarter.com/projects/1300298569/project-phoenix-japans-indie-rpg-feat-aaa-talent"
}, },

View File

@@ -6,7 +6,7 @@ import { PopoverArrow, PopoverContent, PopoverRoot, PopoverTrigger } from 'radix
import LangSwitcherButton from './LangSwitcherButton.vue'; import LangSwitcherButton from './LangSwitcherButton.vue';
import { useStore } from '@nanostores/vue'; import { useStore } from '@nanostores/vue';
import { currentLang } from '@/i18n/store'; import { currentLang, t } from '@/i18n/store';
import { availableLangs } from '@/i18n/ui'; import { availableLangs } from '@/i18n/ui';
const $currentLang = useStore(currentLang); const $currentLang = useStore(currentLang);
@@ -22,7 +22,7 @@ const menuModel = ref(false);
const wrapper = ref<HTMLDivElement | null>(null) const wrapper = ref<HTMLDivElement | null>(null)
const buttonRef = ref<HTMLButtonElement | null>(null) const buttonRef = ref<HTMLButtonElement | null>(null)
const wrapperHovered = useElementHover(wrapper, { delayEnter: 500, delayLeave: 500 }) const wrapperHovered = useElementHover(wrapper, { delayEnter: 100, delayLeave: 500 })
const { focused: buttonFocused } = useFocus(buttonRef) const { focused: buttonFocused } = useFocus(buttonRef)
const isMenuOpen = computed(() => menuModel.value || wrapperHovered.value) const isMenuOpen = computed(() => menuModel.value || wrapperHovered.value)
@@ -39,7 +39,7 @@ watch([wrapperHovered, buttonFocused], (value) => {
<div ref="wrapper" class="wrapper appear-from-top"> <div ref="wrapper" class="wrapper appear-from-top">
<PopoverRoot :open="isMenuOpen" @update:open="() => menuModel = true"> <PopoverRoot :open="isMenuOpen" @update:open="() => menuModel = true">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<button ref="buttonRef" class="btn-round" :class="{ 'active': isMenuOpen }"> <button ref="buttonRef" class="btn-round" :title="t('nav.lang.aria-label')" :class="{ 'active': isMenuOpen }">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M247.15,212.42l-56-112a8,8,0,0,0-14.31,0l-21.71,43.43A88,88,0,0,1,108,126.93,103.65,103.65,0,0,0,135.69,64H160a8,8,0,0,0,0-16H104V32a8,8,0,0,0-16,0V48H32a8,8,0,0,0,0,16h87.63A87.76,87.76,0,0,1,96,116.35a87.74,87.74,0,0,1-19-31,8,8,0,1,0-15.08,5.34A103.63,103.63,0,0,0,84,127a87.55,87.55,0,0,1-52,17,8,8,0,0,0,0,16,103.46,103.46,0,0,0,64-22.08,104.18,104.18,0,0,0,51.44,21.31l-26.6,53.19a8,8,0,0,0,14.31,7.16L148.94,192h70.11l13.79,27.58A8,8,0,0,0,240,224a8,8,0,0,0,7.15-11.58ZM156.94,176,184,121.89,211.05,176Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M247.15,212.42l-56-112a8,8,0,0,0-14.31,0l-21.71,43.43A88,88,0,0,1,108,126.93,103.65,103.65,0,0,0,135.69,64H160a8,8,0,0,0,0-16H104V32a8,8,0,0,0-16,0V48H32a8,8,0,0,0,0,16h87.63A87.76,87.76,0,0,1,96,116.35a87.74,87.74,0,0,1-19-31,8,8,0,1,0-15.08,5.34A103.63,103.63,0,0,0,84,127a87.55,87.55,0,0,1-52,17,8,8,0,0,0,0,16,103.46,103.46,0,0,0,64-22.08,104.18,104.18,0,0,0,51.44,21.31l-26.6,53.19a8,8,0,0,0,14.31,7.16L148.94,192h70.11l13.79,27.58A8,8,0,0,0,240,224a8,8,0,0,0,7.15-11.58ZM156.94,176,184,121.89,211.05,176Z"></path></svg>
</button> </button>
</PopoverTrigger> </PopoverTrigger>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Language } from '@/i18n/ui'; import { useUrlSearchParams } from '@vueuse/core';
import type { Language } from '@/i18n/ui';
import { t } from '@/i18n/store'; import { t } from '@/i18n/store';
import { switchLang } from '@/i18n/utils'; import { switchLang } from '@/i18n/utils';
@@ -12,10 +13,12 @@ defineProps<{
const flagWidth = 18; const flagWidth = 18;
const flagHeight = 18; const flagHeight = 18;
const { lat, lon, zoom } = useUrlSearchParams<{ lat: number; lon: number; zoom: number }>();
</script> </script>
<template> <template>
<a :href="switchLang(currentUrl, lang).pathname" :aria-disabled="disabled" :aria-current="disabled ? 'page' : 'false'" :tabindex="disabled ? -1 : 0"> <a :href="`${switchLang(currentUrl, lang).pathname}?lat=${lat}&lon=${lon}&zoom=${zoom}`" :aria-disabled="disabled" :aria-current="disabled ? 'page' : 'false'" :tabindex="disabled ? -1 : 0">
<div class="flag"> <div class="flag">
<svg v-if="disabled" xmlns="http://www.w3.org/2000/svg" :height="flagHeight" :width="flagWidth" class="valid" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"></path></svg> <svg v-if="disabled" xmlns="http://www.w3.org/2000/svg" :height="flagHeight" :width="flagWidth" class="valid" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"></path></svg>
@@ -46,7 +49,7 @@ a {
&:hover, &:hover,
&:focus-visible { &:focus-visible {
background-color: var(--slate-200); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
} }
&[aria-current="page"] { &[aria-current="page"] {

View File

@@ -36,6 +36,7 @@ function switchMenuMode(newMode: MenuMode) {
onMounted(() => { onMounted(() => {
const mapRef = document.getElementById('world') const mapRef = document.getElementById('world')
if (mapRef) { if (mapRef) {
mapRef.addEventListener('contextmenu', handleContextMenu) mapRef.addEventListener('contextmenu', handleContextMenu)
mapRef.addEventListener('mousedown', hideMenu) mapRef.addEventListener('mousedown', hideMenu)
@@ -188,6 +189,11 @@ function hideMarkerModal() {
function setTitleError(error: Error | null) { function setTitleError(error: Error | null) {
markerTitleInputError.value = error markerTitleInputError.value = error
} }
function handleCoordsCopy() {
markerMenu.value?.dispatchEvent(new CustomEvent('on-copy-coords', { bubbles: true }))
hideMenu()
}
</script> </script>
<template> <template>
@@ -196,7 +202,16 @@ function setTitleError(error: Error | null) {
<menu v-show="shouldBeShown" ref="markerMenu"> <menu v-show="shouldBeShown" ref="markerMenu">
<li> <li>
<button @click="switchMenuMode('editing-marker')"> <button @click="switchMenuMode('editing-marker')">
{{ t('maps.markers.new') }} <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256"><path d="M128,16a88.1,88.1,0,0,0-88,88c0,31.4,14.51,64.68,42,96.25a254.19,254.19,0,0,0,41.45,38.3,8,8,0,0,0,9.18,0A254.19,254.19,0,0,0,174,200.25c27.45-31.57,42-64.85,42-96.25A88.1,88.1,0,0,0,128,16Zm32,96H136v24a8,8,0,0,1-16,0V112H96a8,8,0,0,1,0-16h24V72a8,8,0,0,1,16,0V96h24a8,8,0,0,1,0,16Z"></path></svg>
<span>{{ t('maps.markers.new') }}</span>
</button>
</li>
<li>
<button @click="handleCoordsCopy">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32Zm-8,128H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
<span>{{ t('maps.copyCoords') }}</span>
</button> </button>
</li> </li>
</menu> </menu>
@@ -260,10 +275,11 @@ menu {
padding-block: .25rem; padding-block: .25rem;
top: calc(var(--mouse-y) - .5rem); top: calc(var(--mouse-y) - .5rem);
left: calc(var(--mouse-x) + .75rem); left: calc(var(--mouse-x) + .75rem);
background-color: var(--white); color: var(--foreground);
border: 1px solid var(--slate-400); background-color: var(--background);
border: 1px solid var(--border);
border-radius: .3rem; border-radius: .3rem;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px; box-shadow: var(--shadow);
z-index: 1000; z-index: 1000;
transition-property: top, left; transition-property: top, left;
transition-duration: .16s; transition-duration: .16s;
@@ -271,14 +287,27 @@ menu {
li { li {
a, button { a, button {
display: block; display: flex;
gap: .75ch;
align-items: center;
padding: .33rem 1rem; padding: .33rem 1rem;
padding-left: .66rem;
font-size: .77em; font-size: .77em;
cursor: pointer; cursor: pointer;
width: 100%;
svg {
fill: var(--foreground);
}
&:hover, &:hover,
&:focus-within { &:focus-within {
background-color: var(--slate-100); color: color-mix(in srgb, var(--accent) 90%, var(--background));
background-color: color-mix(in srgb, var(--accent) 10%, var(--background));
svg {
fill: color-mix(in srgb, var(--accent) 75%, var(--background));
}
} }
} }
} }
@@ -289,10 +318,11 @@ dialog {
max-width: calc(100% - 6rem); max-width: calc(100% - 6rem);
padding: 1.5rem 2rem; padding: 1.5rem 2rem;
z-index: 9999; z-index: 9999;
background-color: var(--white); color: var(--foreground);
border: 1px solid var(--slate-400); background-color: var(--background);
border: 1px solid var(--border);
border-radius: .75rem; border-radius: .75rem;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px; box-shadow: var(--shadow);
overflow: visible; overflow: visible;
transform: translateY(1rem); transform: translateY(1rem);
transition: visibility 0s ease-out .3s, opacity .3s ease-out, transform .3s ease-out; transition: visibility 0s ease-out .3s, opacity .3s ease-out, transform .3s ease-out;

View File

@@ -1,22 +1,36 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onUpdated, ref, watch } from 'vue'; import { computed, onUpdated, ref, watch } from 'vue'
import { useElementHover, useFocus } from '@vueuse/core' import { useElementHover, useFocus } from '@vueuse/core'
import { PopoverArrow, PopoverContent, PopoverRoot, PopoverTrigger } from 'radix-vue' import {
import { useStore } from '@nanostores/vue'; PopoverArrow,
PopoverContent,
PopoverRoot,
PopoverTrigger,
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
DialogTrigger
} from 'radix-vue'
import { currentLang } from '@/i18n/store'; import { useStore } from '@nanostores/vue'
import { t } from '@/i18n/store';
const $currentLang = useStore(currentLang); import { currentLang } from '@/i18n/store'
import { t } from '@/i18n/store'
const navModel = ref(false); const $currentLang = useStore(currentLang)
const navModel = ref(false)
const legalModelOpened = ref(false)
const wrapper = ref<HTMLDivElement | null>(null) const wrapper = ref<HTMLDivElement | null>(null)
const buttonRef = ref<HTMLButtonElement | null>(null) const buttonRef = ref<HTMLButtonElement | null>(null)
const wrapperHovered = useElementHover(wrapper, { delayEnter: 500, delayLeave: 500 }) const wrapperHovered = useElementHover(wrapper, { delayEnter: 100, delayLeave: 500 })
const { focused: buttonFocused } = useFocus(buttonRef) const { focused: buttonFocused } = useFocus(buttonRef)
const isNavOpened = computed(() => navModel.value || wrapperHovered.value) const isNavOpened = computed(() => navModel.value || wrapperHovered.value || legalModelOpened.value )
watch([wrapperHovered, buttonFocused], (value) => { watch([wrapperHovered, buttonFocused], (value) => {
// Check if all values from array are false // Check if all values from array are false
@@ -25,7 +39,7 @@ watch([wrapperHovered, buttonFocused], (value) => {
} }
}) })
let url: URL | null = null; let url: URL | null = null
onUpdated(() => { onUpdated(() => {
url = new URL(window.location.href) url = new URL(window.location.href)
@@ -80,8 +94,7 @@ const menus: Menu[] = [
{ {
name: 'Mines Blanches', name: 'Mines Blanches',
img: '/images/aldys-cantane-mines-blanches-cover.png', img: '/images/aldys-cantane-mines-blanches-cover.png',
url: `/${$currentLang.value}/aldys/cantane/mines-blanches`, url: `/${$currentLang.value}/aldys/cantane/mines-blanches`
newItem: true
}, },
] ]
} }
@@ -92,7 +105,7 @@ const menus: Menu[] = [
<div class="wrapper" ref="wrapper"> <div class="wrapper" ref="wrapper">
<PopoverRoot :open="isNavOpened" @update:open="() => navModel = true"> <PopoverRoot :open="isNavOpened" @update:open="() => navModel = true">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<button ref="buttonRef" class="btn-round" :class="{ 'active': isNavOpened }"> <button ref="buttonRef" class="btn-round" :title="t('nav.map.aria-label')" :class="{ 'active': isNavOpened }">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM96,176a8,8,0,0,0-1.94.24L40,189.75V62.25L95.07,48.48l.93.46Zm120,17.75-55.07,13.77-.93-.46V80a8,8,0,0,0,1.94-.23L216,66.25Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM96,176a8,8,0,0,0-1.94.24L40,189.75V62.25L95.07,48.48l.93.46Zm120,17.75-55.07,13.77-.93-.46V80a8,8,0,0,0,1.94-.23L216,66.25Z"/></svg>
</button> </button>
</PopoverTrigger> </PopoverTrigger>
@@ -130,6 +143,26 @@ const menus: Menu[] = [
</li> </li>
</menu> </menu>
</template> </template>
<div class="legal">
<DialogRoot v-model:open="legalModelOpened">
<DialogTrigger as-child>
<button :href="`/${$currentLang}/legal`">
{{ t('legal.cta') }}
</button>
</DialogTrigger>
<DialogPortal>
<DialogOverlay class="dialog-overlay" />
<DialogContent class="dialog-content">
<DialogTitle class="dialog-title">
{{ t('legal.cta') }}
</DialogTitle>
<DialogDescription class="dialog-description" v-html="t('legal.text')" />
</DialogContent>
</DialogPortal>
</DialogRoot>
</div>
</PopoverContent> </PopoverContent>
</PopoverRoot> </PopoverRoot>
</div> </div>
@@ -151,6 +184,38 @@ const menus: Menu[] = [
margin-left: .25rem; margin-left: .25rem;
} }
.legal {
margin-top: 1em;
display: flex;
align-items: center;
width: fit-content;
font-size: .8em;
text-underline-offset: 2px;
text-decoration: underline;
gap: .5ch;
button {
cursor: pointer;
}
&:hover,
&:focus-visible {
text-decoration: none;
color: var(--green-500);
}
&::before {
content: url('/icon/question-mark.svg');
display: inline-block;
width: 1em;
height: 1em;
}
}
.dark .legal::before {
content: url('/icon/question-mark-dark.svg');
}
.map-menu { .map-menu {
margin-top: .25rem; margin-top: .25rem;
margin-bottom: .5rem; margin-bottom: .5rem;
@@ -189,7 +254,7 @@ const menus: Menu[] = [
img { img {
display: block; display: block;
border-radius: .3rem; border-radius: .3rem;
border: .1rem solid var(--white); border: .1rem solid var(--background);
outline: .15rem solid transparent; outline: .15rem solid transparent;
} }
@@ -202,14 +267,14 @@ const menus: Menu[] = [
pointer-events: none; pointer-events: none;
.fill { fill: var(--orange-500); } .fill { fill: var(--orange-500); }
.stroke { fill: var(--white); } .stroke { fill: var(--background); }
} }
} }
figcaption { figcaption {
font-size: .75em; font-size: .75em;
text-align: center; text-align: center;
color: var(--slate-500); color: color-mix(in srgb, var(--foreground) 80%, var(--background));
} }
} }
} }

View File

@@ -20,6 +20,7 @@ const props = defineProps<{
players: PlayerMarker, players: PlayerMarker,
mapKey?: string, mapKey?: string,
searchConfig?: SearchConfig searchConfig?: SearchConfig
disableTags?: boolean
}>() }>()
const customMarkersKey = props.mapKey ? `custom-markers-${props.mapKey}` : 'custom-markers' const customMarkersKey = props.mapKey ? `custom-markers-${props.mapKey}` : 'custom-markers'
@@ -209,7 +210,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
</script> </script>
<template> <template>
<nav ref="searchBarWrapper" class="toolbar appear-from-top" :key="navKey"> <nav ref="searchBarWrapper" :aria-label="t('nav.aria-label')" class="toolbar appear-from-top" :key="navKey">
<SearchMapSwitch /> <SearchMapSwitch />
<div ref="searchBar" class="search-w" :data-focused="shouldBeActive"> <div ref="searchBar" class="search-w" :data-focused="shouldBeActive">
@@ -286,6 +287,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
</div> </div>
<SearchMarkersTags <SearchMarkersTags
v-if="!props.disableTags"
:current-search-mode="currentSearchMode" :current-search-mode="currentSearchMode"
:custom-markers="customMarkersData" :custom-markers="customMarkersData"
:search-config="props.searchConfig" :search-config="props.searchConfig"
@@ -306,11 +308,15 @@ function resetAllFields(actionAfter?: "focusAfter") {
margin-left: calc(45px + .5rem); margin-left: calc(45px + .5rem);
padding: .5rem 1.2rem; padding: .5rem 1.2rem;
width: calc(100% - 3rem); width: calc(100% - 3rem);
background: var(--white); color: var(--foreground);
border: 1px solid var(--slate-400); background: var(--background);
border: 1px solid var(--border);
border-radius: 25px; border-radius: 25px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; box-shadow: var(--shadow);
pointer-events: all; pointer-events: all;
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: var(--timing-function);
@media screen and (width >= 900px) { @media screen and (width >= 900px) {
width: clamp(25rem, 35dvw, 30rem); width: clamp(25rem, 35dvw, 30rem);
@@ -336,7 +342,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
flex-grow: 1; flex-grow: 1;
&::placeholder { &::placeholder {
color: var(--slate-400); color: color-mix(in srgb, var(--foreground) 75%, var(--background));
} }
} }
@@ -346,7 +352,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
transform: translateY(-50%); transform: translateY(-50%);
left: 0; left: 0;
width: $search-icon-size; width: $search-icon-size;
fill: var(--slate-400); fill: color-mix(in srgb, var(--foreground) 75%, var(--background));
pointer-events: none; pointer-events: none;
} }
@@ -373,16 +379,20 @@ function resetAllFields(actionAfter?: "focusAfter") {
} }
&:hover { &:hover {
.icon {
color: var(--foreground);
}
&::before { &::before {
background-color: var(--slate-300); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
} }
} }
&:focus-visible { &:focus-visible {
&::before { &::before {
background-color: var(--slate-200); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
outline: 1px dotted var(--slate-500); outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
outline: 4px auto var(--slate-900); outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
} }
} }
} }
@@ -397,22 +407,22 @@ function resetAllFields(actionAfter?: "focusAfter") {
&:hover { &:hover {
&::before { &::before {
background-color: var(--slate-300); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
} }
} }
&:focus-visible { &:focus-visible {
&::before { &::before {
background-color: var(--slate-200); background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
outline: 1px dotted var(--slate-500); outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
outline: 4px auto var(--slate-900); outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
} }
} }
} }
.close-btn { .close-btn {
font-size: 1.1em; font-size: 1.1em;
color: var(--slate-500); color: color-mix(in srgb, var(--foreground) 75%, var(--background));
} }
} }
@@ -434,7 +444,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: var(--slate-300); background-color: color-mix(in srgb, var(--foreground) 30%, var(--background));
} }
> * + * { > * + * {
@@ -451,7 +461,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
} }
&:hover:not(:has(a:hover)) { &:hover:not(:has(a:hover)) {
background-color: var(--slate-100); background-color: color-mix(in srgb, var(--foreground) 10%, var(--background));
.title { .title {
text-decoration: underline; text-decoration: underline;
@@ -488,9 +498,14 @@ function resetAllFields(actionAfter?: "focusAfter") {
.title { .title {
font-weight: 500; font-weight: 500;
text-underline-offset: 2px; text-underline-offset: 2px;
& + .desc {
margin-top: .25rem;
}
} }
.desc { .desc {
color: var(--slate-500); color: color-mix(in srgb, var(--foreground) 50%, var(--background));
.map-link { .map-link {
margin-top: .5rem; margin-top: .5rem;
@@ -526,12 +541,12 @@ function resetAllFields(actionAfter?: "focusAfter") {
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
right: .75rem; right: .75rem;
fill: var(--slate-400); fill: color-mix(in srgb, var(--foreground) 70%, var(--background));
} }
&:focus-visible { &:focus-visible {
outline: 1px dotted var(--slate-500); outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
outline: 4px auto var(--slate-900); outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
.title { .title {
text-decoration: underline; text-decoration: underline;
@@ -598,8 +613,9 @@ function resetAllFields(actionAfter?: "focusAfter") {
&[data-focused="true"] { &[data-focused="true"] {
margin-bottom: 1em; margin-bottom: 1em;
.input-w { .input-w {
border-bottom: 1px solid var(--slate-200); border-bottom: 1px solid color-mix(in srgb, var(--foreground) 20%, var(--background));
margin-bottom: .5rem; margin-bottom: .5rem;
padding-bottom: .5rem; padding-bottom: .5rem;

View File

@@ -125,10 +125,11 @@ function emitCategorySwitch(newCategory: MapMarkerGroup) {
padding-inline: .8rem; padding-inline: .8rem;
font-weight: 600; font-weight: 600;
font-size: .85em; font-size: .85em;
background: var(--white); color: var(--foreground);
border: 1px solid var(--slate-400); background: var(--background);
border: 1px solid var(--border);
border-radius: 100vmax; border-radius: 100vmax;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; box-shadow: var(--shadow);
white-space: nowrap; white-space: nowrap;
pointer-events: all; pointer-events: all;
cursor: pointer; cursor: pointer;
@@ -145,6 +146,10 @@ function emitCategorySwitch(newCategory: MapMarkerGroup) {
width: 1.1em; width: 1.1em;
} }
svg {
fill: var(--foreground);
}
.label { .label {
text-underline-offset: .15rem; text-underline-offset: .15rem;
} }
@@ -154,12 +159,12 @@ function emitCategorySwitch(newCategory: MapMarkerGroup) {
@each $c in $colors { @each $c in $colors {
&.#{$c} { &.#{$c} {
&.active { &.active {
color: var(--white); color: var(--background);
background-color: var(--#{$c}-500); background-color: var(--#{$c}-500);
border-color: var(--#{$c}-700); border-color: var(--#{$c}-700);
.icon { .icon {
fill: var(--white); fill: var(--background);
} }
} }

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import { isVNode, onMounted } from "vue"
import { ToastLifetime, useToast, type Toast } from './useToast.ts'
import { ToastDescription, ToastProvider, ToastRoot, ToastTitle, ToastViewport } from 'radix-vue'
const { toast, toasts } = useToast()
import { t } from '@/i18n/store';
onMounted(() => {
window.addEventListener('on-toast-push', (e) => handleInserted(e as CustomEvent))
})
function handleInserted(e: CustomEvent) {
if ( e.detail.preset) {
switch (e.detail.preset) {
case "copied-coords":
default:
toast({
description: t('toast.copyCoords.description'),
duration: ToastLifetime.SHORT,
variant: 'success'
})
break
}
return
}
const toastData = e.detail.toast as Toast
toast(toastData)
}
function handleUpdate(id: string) {
toasts.value.forEach((t, i) => {
if (t.id === id) {
t.open = false
}
})
}
</script>
<template>
<ToastProvider>
<ToastRoot
class="toast-root"
:class="toast.variant ? `toast-${toast.variant}` : ''"
v-for="toast in toasts"
:key="toast.id"
v-bind="toast"
@update:open="handleUpdate(toast.id)"
>
<div v-if="toast.variant === 'success'" class="toast-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256"><path d="M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z"></path></svg>
</div>
<ToastTitle class="toast-title" v-if="toast.title">
{{ toast.title }}
</ToastTitle>
<template v-if="toast.description">
<ToastDescription class="toast-description" v-if="isVNode(toast.description)">
<component :is="toast.description" />
</ToastDescription>
<ToastDescription v-else class="toast-description">
{{ toast.description }}
</ToastDescription>
</template>
</ToastRoot>
<ToastViewport class="toast-viewport"/>
</ToastProvider>
</template>

View File

@@ -0,0 +1,176 @@
import { computed, ref } from "vue"
import type { Component, VNode } from "vue"
import type { ToastRootProps } from "radix-vue"
const TOAST_LIMIT = 3
const TOAST_REMOVE_DELAY = 1000000
export interface ToastProps extends ToastRootProps {
onOpenChange?: ((value: boolean) => void) | undefined
}
export enum ToastLifetime {
SHORT = 2000,
MEDIUM = 3500,
LONG = 6000,
}
export type StringOrVNode =
| string
| VNode
| (() => VNode)
type ToasterToast = ToastProps & {
id: string
title?: string
description?: StringOrVNode
action?: Component,
variant?: "default" | "success" | "destructive"
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
function addToRemoveQueue(toastId: string) {
if (toastTimeouts.has(toastId))
return
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: actionTypes.REMOVE_TOAST,
toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
const state = ref<State>({
toasts: [],
})
function dispatch(action: Action) {
switch (action.type) {
case actionTypes.ADD_TOAST:
state.value.toasts = [action.toast, ...state.value.toasts].slice(0, TOAST_LIMIT)
break
case actionTypes.UPDATE_TOAST:
state.value.toasts = state.value.toasts.map(t =>
t.id === action.toast.id ? { ...t, ...action.toast } : t,
)
break
case actionTypes.DISMISS_TOAST: {
const { toastId } = action
if (toastId) {
addToRemoveQueue(toastId)
}
else {
state.value.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
state.value.toasts = state.value.toasts.map(t =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t,
)
break
}
case actionTypes.REMOVE_TOAST:
if (action.toastId === undefined)
state.value.toasts = []
else
state.value.toasts = state.value.toasts.filter(t => t.id !== action.toastId)
break
}
}
function useToast() {
return {
toasts: computed(() => state.value.toasts),
toast,
dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
}
}
export type Toast = Omit<ToasterToast, "id">
function toast(props: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: actionTypes.UPDATE_TOAST,
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
dispatch({
type: actionTypes.ADD_TOAST,
toast: {
...props,
id,
open: true,
onOpenChange: (open: boolean) => {
if (!open)
dismiss()
},
},
})
return {
id,
dismiss,
update,
}
}
export { toast, useToast }

12
src/env.d.ts vendored
View File

@@ -1,2 +1,14 @@
/// <reference path="../.astro/types.d.ts" /> /// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
interface ImportMetaEnv {
readonly ENABLE_TOOLBAR: boolean
readonly ENABLE_TOOLBAR_TAGS: boolean
readonly ENABLE_CUSTOM_MARKERS: boolean
readonly ENABLE_BREADCRUMB: boolean
readonly ENABLE_I18N: boolean
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -21,6 +21,9 @@ export const translations: LanguageDict = {
'capitals': 'Capitales', 'capitals': 'Capitales',
'points-of-interest': 'Points d\'intérêt', 'points-of-interest': 'Points d\'intérêt',
'personals': 'Personnels', 'personals': 'Personnels',
'nav.aria-label': 'Navigation principale',
'nav.map.aria-label': 'Changer de carte',
'nav.lang.aria-label': 'Changer de langue',
'maps.go-to-map': 'Voir la carte détaillée', 'maps.go-to-map': 'Voir la carte détaillée',
'maps.go-to-player': 'Aller à la position actuelle des joueurs', 'maps.go-to-player': 'Aller à la position actuelle des joueurs',
'maps.seeQuest': 'Voir la quête', 'maps.seeQuest': 'Voir la quête',
@@ -28,9 +31,23 @@ export const translations: LanguageDict = {
'maps.search': 'Rechercher la carte', 'maps.search': 'Rechercher la carte',
'maps.searchPlaceholder': "Ville, point d'intérêt…", 'maps.searchPlaceholder': "Ville, point d'intérêt…",
'maps.closeSearch': 'Enlever le filtre', 'maps.closeSearch': 'Enlever le filtre',
'maps.copyCoords': 'Copier les coordonnées',
'maps.markers.new': 'Nouveau marqueur', 'maps.markers.new': 'Nouveau marqueur',
'maps.markers.addPersonal': 'Ajouter un marqueur personnel', 'maps.markers.addPersonal': 'Ajouter un marqueur personnel',
'maps.markers.newNotice': "Le marqueur sera sauvegardé mais n'apparaîtra que sur votre carte !" 'maps.markers.newNotice': "Le marqueur sera sauvegardé mais n'apparaîtra que sur votre carte !",
'toast.copyCoords.description': 'Les coordonnées ont été copiées dans le presse-papiers !',
'legal.cta': 'Mentions légales',
'legal.text': `
<p>Cette application est construite avec <a href="https://astro.build/" target="_blank">Astro</a>, <a href="https://leafletjs.com/" target="_blank">Leaflet</a> et <a href="https://vuejs.org/" target="_blank">Vue</a>. J'utilise également des icônes de <a href="https://phosphoricons.com/" target="_blank">Phosphor</a> et elle est actuellement hébergée sur <a href="https://www.ovhcloud.com" target="_blank">OVH</a>.</p>
<p>En ce qui concerne les œuvres d'art que j'utilise sur les marqueurs, j'essaie de les sourcer du mieux que je peux. Si vous connaissez la source originale d'une œuvre d'art que je n'indique pas, veuillez me contacter par mail.</p>
<p>Je n'inclus pas d'images d'IA, car <strong>je suis farouchement opposé à l'IA générative</strong> pour la création de contenu. Si vous voyez une image qui semble être de l'IA sur ce site, faites-le-moi savoir immédiatement.</p>
<h3>Données personnelles</h3>
<p>Je ne collecte aucune donnée sur votre activité dans cette application. Les marqueurs personnalisés que vous pouvez placer n'existent que sur votre navigateur en tant que données de <a href="https://developer.mozilla.org/fr/docs/Web/API/Window/localStorage" target="_blank">local storage</a>.</p>
<p>Mon hébergeur collecte des données de trafic pour des raisons de sécurité ; comme les adresses IP et leur emplacement approximatif, les types de navigateurs, les agents utilisateurs (si vous êtes un robot ou un humain) et l'état de la demande. </p>
<p>Ce site n'utilise pas de pixels de suivi ou d'analyse, et je n'utilise aucune autre application tierce qui pourrait collecter des données sur vous (comme Youtube ou autre).</p>
<p>Malgré cela, <strong>aucune de vos données n'est vendue ou transmise à un tiers quel qu'il soit</strong>.</p>
<p>Si vous avez des questions, vous pouvez me contacter à <a href="mailto:contact@alexcreates.fr">contact@alexcreates.fr</a>.</p>
`
}, },
'en': { 'en': {
'lang.fr': 'Français', 'lang.fr': 'Français',
@@ -48,6 +65,9 @@ export const translations: LanguageDict = {
'capitals': 'Capitals', 'capitals': 'Capitals',
'points-of-interest': 'Points of interest', 'points-of-interest': 'Points of interest',
'personals': 'Personals', 'personals': 'Personals',
'nav.aria-label': 'Main navigation',
'nav.map.aria-label': 'Switch map',
'nav.lang.aria-label': 'Switch language',
'maps.go-to-map': 'See detailed map', 'maps.go-to-map': 'See detailed map',
'maps.go-to-player': 'Go to current players position', 'maps.go-to-player': 'Go to current players position',
'maps.seeQuest': 'See the quest', 'maps.seeQuest': 'See the quest',
@@ -55,8 +75,22 @@ export const translations: LanguageDict = {
'maps.search': 'Search the map', 'maps.search': 'Search the map',
'maps.searchPlaceholder': "City, point of interest…", 'maps.searchPlaceholder': "City, point of interest…",
'maps.closeSearch': 'Remove filter', 'maps.closeSearch': 'Remove filter',
'maps.copyCoords': 'Copy coordinates',
'maps.markers.new': 'New marker', 'maps.markers.new': 'New marker',
'maps.markers.addPersonal': 'Add a personal marker', 'maps.markers.addPersonal': 'Add a personal marker',
'maps.markers.newNotice': "The marker will be saved but will only appear on your map!" 'maps.markers.newNotice': "The marker will be saved but will only appear on your map!",
'toast.copyCoords.description': 'Coordinates have been copied to your clipboard !',
'legal.cta': 'Legal notice',
'legal.text': `
<p>This app is built with <a href="https://astro.build/" target="_blank">astro</a>, <a href="https://leafletjs.com/" target="_blank">leaflet</a> and <a href="https://vuejs.org/" target="_blank">vue</a>. I also use icons from <a href="https://phosphoricons.com/" target="_blank">phosphor</a> and it's currently hosted on <a href="https://www.ovhcloud.com" target="_blank">OVH</a>.</p>
<p>For the artworks I use on the markers, I try to source them to the best of my knowledge, from what I can scrap off the internet. If you know the original source of an artwork, please contact me by email.</p>
<p>I don't include AI images as <strong>I'm vehemently opposed to generative AI</strong> for content creation. If you see an image that looks like it might be AI on this site, let me know immediately.</p>
<h3>Privacy</h3>
<p>I don't collect any data of your activity with this app. The custom markers that you may place only exist on your browser as <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" target="_blank">local storage</a> data.</p>
<p>My hosting provider collects traffic data for security reasons ; like IP addresses and their approximate location, browsers types, user agents (if you're a robot or a human) and the request status.</p>
<p>This website doesn't use any tracking pixels or analytics, and I don't use any other third-party app that might collect stuff from you (like Youtube or anything else)</p>
<p>Regardless, <strong>none of your data is sold or passed to any third-party whatsoever</strong>.</p>
<p>If you have any questions, you can contact me at <a href="mailto:contact@alexcreates.fr">contact@alexcreates.fr</a>.</p>
`
} }
} as const; } as const;

View File

@@ -28,7 +28,7 @@ const lang = getLangFromUrl(Astro.url) || defaultLang;
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="author" content="Alexis Pelé" /> <meta name="author" content="Alexis Pelé" />
{head?.description && <meta name="description" content={head.description} /> } {head?.description && <meta name="description" content={head.description} /> }
<meta name="viewport" content="width=device-width; initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>{title} — Leim Wiki</title> <title>{title} — Leim Wiki</title>
@@ -59,6 +59,54 @@ const lang = getLangFromUrl(Astro.url) || defaultLang;
<script is:inline type="text/javascript" src="/js/leaflet-measure.min.js"></script> <script is:inline type="text/javascript" src="/js/leaflet-measure.min.js"></script>
</head> </head>
<body> <body>
<script is:inline>
let initialTheme = localStorage.getItem("theme");
const darkModeMq = window.matchMedia("(prefers-color-scheme: dark)");
// Initial paint from localstorage
switch (initialTheme) {
case "light":
enableLightTheme();
break;
case "system":
resetTheme();
break;
case "dark":
default:
enableDarkTheme();
break;
}
// Enables dark theme and updates preferences
function enableDarkTheme() {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
localStorage.setItem("theme", "dark");
}
// Enables light theme and updates preferences
function enableLightTheme() {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
localStorage.setItem("theme", "light");
}
// Reset theme to OS values
function resetTheme() {
const prefersDark = darkModeMq.matches;
if (prefersDark) {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
} else {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
}
localStorage.setItem("theme", "system");
}
</script>
<slot /> <slot />
</body> </body>
</html> </html>

View File

@@ -17,6 +17,7 @@ import MapOverlay from '@/components/maps/MapOverlay.astro'
]} ]}
/> />
<Map <Map
title='Borélis'
mapKey='aldys-borelis' mapKey='aldys-borelis'
zoomifyKey='fr/borelis' zoomifyKey='fr/borelis'
mapHeight={3710} mapHeight={3710}

View File

@@ -24,6 +24,7 @@ const markers = markersData.data as MapMarker[]
]} ]}
/> />
<Map <Map
title='Cantane'
mapKey='aldys-cantane' mapKey='aldys-cantane'
zoomifyKey='fr/cantane' zoomifyKey='fr/cantane'
mapHeight={4124} mapHeight={4124}

View File

@@ -25,6 +25,7 @@ const markers = markersData.data as MapMarker[]
]} ]}
/> />
<Map <Map
title='White Hollow'
mapKey='aldys-cantane-mines' mapKey='aldys-cantane-mines'
zoomifyKey='fr/mines-blanches' zoomifyKey='fr/mines-blanches'
mapHeight={6020} mapHeight={6020}

View File

@@ -22,6 +22,7 @@ const players = playersData as PlayerMarker
]} ]}
/> />
<Map <Map
title='Bamast'
mapKey='bamast' mapKey='bamast'
zoomifyKey='fr/bamast' zoomifyKey='fr/bamast'
mapHeight={18342} mapHeight={18342}

View File

@@ -30,6 +30,7 @@ const players = playersData as PlayerMarker
]} ]}
/> />
<Map <Map
title='Aldys'
mapKey='aldys' mapKey='aldys'
zoomifyKey='en/aldys-nord' zoomifyKey='en/aldys-nord'
mapHeight={21896} mapHeight={21896}

View File

@@ -17,6 +17,7 @@ import MapOverlay from '@/components/maps/MapOverlay.astro'
]} ]}
/> />
<Map <Map
title='Borélis'
mapKey='aldys-borelis' mapKey='aldys-borelis'
zoomifyKey='fr/borelis' zoomifyKey='fr/borelis'
mapHeight={3710} mapHeight={3710}

View File

@@ -24,6 +24,7 @@ const markers = markersData.data as MapMarker[]
]} ]}
/> />
<Map <Map
title='Cantane'
mapKey='aldys-cantane' mapKey='aldys-cantane'
zoomifyKey='fr/cantane' zoomifyKey='fr/cantane'
mapHeight={4124} mapHeight={4124}

View File

@@ -25,6 +25,7 @@ const markers = markersData.data as MapMarker[]
]} ]}
/> />
<Map <Map
title='Les Mines Blanches'
mapKey='aldys-cantane-mines' mapKey='aldys-cantane-mines'
zoomifyKey='fr/mines-blanches' zoomifyKey='fr/mines-blanches'
mapHeight={6020} mapHeight={6020}

View File

@@ -22,6 +22,7 @@ const players = playersData as PlayerMarker
]} ]}
/> />
<Map <Map
title='Bamast'
mapKey='bamast' mapKey='bamast'
zoomifyKey='fr/bamast' zoomifyKey='fr/bamast'
mapHeight={18342} mapHeight={18342}

View File

@@ -30,6 +30,7 @@ const players = playersData as PlayerMarker
]} ]}
/> />
<Map <Map
title='Aldys'
mapKey='aldys' mapKey='aldys'
zoomifyKey='fr/aldys-nord' zoomifyKey='fr/aldys-nord'
mapHeight={21896} mapHeight={21896}

View File

@@ -7,26 +7,90 @@ export type MapCoords = {
} }
export type MapMarker = { export type MapMarker = {
/**
* The title of the marker (must be unique).
*/
title: string, title: string,
/**
* The coordinates of the marker.
* @example { x: 123, y: 456 }
*/
markerCoords: MapCoords, markerCoords: MapCoords,
/**
* The description of the marker.
*/
description?: string, description?: string,
/**
* The link that can appear on the marker title.
* @example https://example.com
*/
link?: string, link?: string,
/**
* The ID of the map the marker is supposed to point to.
*
* This should be a valid path of the `maps/data` folder.
* @example "fr/aldys/borelis"
*/
mapId?: string, mapId?: string,
/**
* The group that the marker belongs to.
* @example "capitals"
*/
group?: MapMarkerGroup, group?: MapMarkerGroup,
/**
* The icon that the marker should use.
* @example "castle"
*/
icon?: MapMarkerIcon, icon?: MapMarkerIcon,
/**
* Cover image for the marker.
* This should be a valid image in the public folder.
*
* @example "Ambrose.webp"
*/
cover?: string, cover?: string,
/**
* The link to the cover image's source.
*/
coverLink?: string, coverLink?: string,
/**
* The author of the cover image.
*/
coverAuthor?: string, coverAuthor?: string,
/**
* Whether the cover image should be displayed in portrait mode.
*/
coverPortrait?: boolean coverPortrait?: boolean
} }
/**
* A marker that is used to display players' location on the map.
*/
export type PlayerMarker = { export type PlayerMarker = {
/**
* The title of the marker (must be unique).
*/
title: string, title: string,
/**
* The coordinates of the marker.
* @example { x: 123, y: 456 }
*/
markerCoords: MapCoords, markerCoords: MapCoords,
/**
* The description of the marker.
*/
description?: string, description?: string,
/**
* The link that can appear on the marker title.
* @example https://example.com
*/
link?: string, link?: string,
} }
/**
* A marker that is used to display a custom location on the map.
* These are only used on the client side and are not saved anywhere else.
*/
export type CustomMarker = { export type CustomMarker = {
lat: number, lat: number,
lon: number, lon: number,

View File

@@ -1,33 +1,106 @@
import type { MapMarker, PlayerMarker } from "./Leaflet" import type { MapMarker, PlayerMarker } from "./Leaflet"
/**
* A breadcrumb item.
*/
export interface BreadcrumbItem { export interface BreadcrumbItem {
name: string; /**
url: string; * The label of the breadcrumb.
*/
name: string
/**
* The URL the breadcrumb points to.
*/
url: string
} }
/**
* Search configuration options for the map overlay.
* This is used to filter the markers on the map.
*/
export interface SearchConfig { export interface SearchConfig {
/** Whether to disable the quests in the search bar. */
disableQuests?: boolean disableQuests?: boolean
/** Whether to disable the capitals in the search bar. */
disableCapitals?: boolean disableCapitals?: boolean
/** Whether to disable the landmarks in the search bar. */
disableLandmarks?: boolean disableLandmarks?: boolean
} }
export interface MapProps { export interface MapProps {
title?: string
/**
* The ID of the map.
* This is used to identify the map and store custom markers in the local storage.
* @example "fr/aldys/borelis"
*/
mapKey: string mapKey: string
/**
* The ID of the map in the zoomify folder.
*/
zoomifyKey: string zoomifyKey: string
/**
* Height of the map in pixels.
* This information is generated by the zoomify tool.
* @example 30000
*/
mapHeight: number mapHeight: number
/**
* Width of the map in pixels.
* This information is generated by the zoomify tool.
* @example 30000
*/
mapWidth: number mapWidth: number
/**
* List of markers to display on the map.
*/
markers?: MapMarker[] markers?: MapMarker[]
/**
* Current player position on the map.
*/
players?: PlayerMarker players?: PlayerMarker
/**
* Main unit of the map.
*/
rulerMainUnit?: string rulerMainUnit?: string
/**
* Ratio used to convert the map units to their relevant unit.
*/
rulerDistanceRatio?: number rulerDistanceRatio?: number
/**
* Whether to show the walking distance in the ruler.
*/
rulerHideWalkDistance?: boolean rulerHideWalkDistance?: boolean
/**
* Default background color of the map.
* @exemple "#FFF"
*/
backgroundColor?: string backgroundColor?: string
} }
export interface MapOverlayProps { export interface MapOverlayProps {
/**
* The ID of the map.
* This is used to identify the map and store custom markers in the local storage.
* @example "fr/aldys/borelis"
*/
mapKey: string mapKey: string
/**
* List of markers to display on the map.
*/
markers?: MapMarker[] markers?: MapMarker[]
/**
* Current player position on the map.
*/
players?: PlayerMarker players?: PlayerMarker
/**
* Configuration for the search bar.
* This is used to filter the markers on the map.
* @example { disableQuests: true, disableCapitals: false, disableLandmarks: false }
*/
searchConfig?: SearchConfig searchConfig?: SearchConfig
/**
* List of breadcrumbs to display.
*/
breadcrumbs?: BreadcrumbItem[] breadcrumbs?: BreadcrumbItem[]
} }