18 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
28 changed files with 1140 additions and 732 deletions

View File

@@ -1,5 +1,15 @@
# 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
Live demo available at https://maps.alexcreates.fr
## Features
* 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,7 +1,7 @@
{
"name": "leim-maps",
"type": "module",
"version": "1.4.9",
"version": "1.4.11",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
@@ -10,18 +10,18 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/sitemap": "^3.4.0",
"@astrojs/sitemap": "^3.4.1",
"@astrojs/vue": "^5.1.0",
"@nanostores/persistent": "1.0.0",
"@nanostores/vue": "^1.0.0",
"@types/leaflet": "^1.9.18",
"@vueuse/core": "^13.2.0",
"astro": "^5.8.0",
"@vueuse/core": "^13.4.0",
"astro": "^5.10.0",
"nanostores": "^1.0.1",
"radix-vue": "^1.9.17",
"vue": "^3.5.14"
"vue": "^3.5.17"
},
"devDependencies": {
"sass": "^1.89.0"
"sass": "^1.89.2"
}
}

1218
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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

View File

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

View File

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

View File

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

View File

@@ -14,15 +14,15 @@
display: block;
width: 100%;
padding: .4rem .5rem;
border: 1px solid var(--slate-300);
border: 1px solid color-mix(in srgb, var(--foreground) 30%, var(--background));
font-size: .8em;
border-radius: .2rem;
border-radius: var(--border-radius);
transition-property: box-shadow;
transition-duration: .2s;
transition-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
transition-timing-function: var(--timing-function);
&: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,7 +10,9 @@
.leaflet-map-pane {
.leaflet-popup-pane {
.leaflet-popup-content-wrapper {
border-radius: .25rem;
color: var(--foreground);
background-color: var(--background);
border-radius: var(--border-radius);
overflow: clip;
.leaflet-popup-content {
@@ -22,6 +24,7 @@
.leaflet-popup-tip {
width: .8rem;
height: .8rem;
background-color: var(--background);
}
}
@@ -29,15 +32,18 @@
.leaflet-tooltip {
&.capital-name {
padding: .2rem .4rem;
color: #EEE;
color: var(--foreground);
font-weight: bold;
background: none;
border: none;
box-shadow: none;
border-bottom-left-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 {
display: none;
@@ -47,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

@@ -23,16 +23,16 @@
grid-template-columns: auto max-content;
column-gap: 15px;
align-items: center;
color: var(--slate-800);
border: 1px solid var(--slate-400);
background-color: var(--white);
border-radius: 6px;
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(--white));
background-color: color-mix(in srgb, var(--green-500) 17.5%, var(--background));
.toast-icon svg {
fill: var(--green-700);
@@ -51,20 +51,20 @@
}
}
.toast-root[data-state='open'] {
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
animation: slideIn 150ms var(--timing-function);
}
.toast-root[data-state='closed'] {
animation: hide 100ms ease-in;
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 ease-out;
transition: transform 200ms var(--timing-function);
}
.toast-root[data-swipe='end'] {
animation: swipeOut 100ms ease-out;
animation: swipeOut 100ms var(--timing-function);
}
@keyframes hide {

View File

@@ -10,6 +10,7 @@
@use 'map';
@use 'radix/toast';
@use 'radix/dialog';
:root {
--white: #fff;
@@ -36,19 +37,34 @@
--orange-700: #c2410c;
--purple-500: #8b5cf6;
--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 {
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'] {
padding-right: 1.25em;
white-space: nowrap;
orphans: 2;
}
a[href^='http']::after {
position: absolute;
content: url('/icon/square-arrow-out.svg');
display: inline-block;
width: 1em;
@@ -56,6 +72,16 @@ a[href^='http']::after {
margin-left: .25em;
}
.dark {
a[href^='http']::after {
content: url('/icon/square-arrow-out-dark.svg');
}
}
strong {
font-weight: 600;
}
.icon {
width: 1em;
}

View File

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

@@ -8,6 +8,7 @@ import SearchMarkers from "./overlay/SearchMarkers.vue";
import LangSwitcher from "./overlay/LangSwitcher.vue";
import MapOverlayBreadcrumbs from "./MapOverlayBreadcrumbs.astro";
import ToastService from "./overlay/ToastService.vue";
import ThemeSwitch from "../global/ThemeSwitch.vue";
interface Props extends MapOverlayProps {}
@@ -41,9 +42,14 @@ const currentUrl = Astro.url
</SearchMarkers>
}
{ENABLE_I18N &&
<LangSwitcher currentUrl={currentUrl} client:only="vue" />
}
<div class="right-menu">
{ENABLE_I18N &&
<LangSwitcher currentUrl={currentUrl} client:only="vue" />
}
<ThemeSwitch client:only="vue" />
</div>
</div>
{ENABLE_BREADCRUMB &&
@@ -71,5 +77,15 @@ const currentUrl = Astro.url
align-items: top;
gap: 1rem;
}
.right-menu {
display: grid;
gap: .5rem;
align-content: flex-start;
@media screen and (width >= 900px) {
display: flex;
}
}
}
</style>

View File

@@ -14,7 +14,7 @@ const { breadcrumbs = [] } = Astro.props
{breadcrumbs.map((breadcrumb, index) => (
<li>
{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">
<h1 class="label">
@@ -23,7 +23,7 @@ const { breadcrumbs = [] } = Astro.props
</span>
}
{!(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 }} data-astro-prefetch="hover">
<span class="label">
@@ -31,7 +31,7 @@ const { breadcrumbs = [] } = Astro.props
</span>
</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>
))}
@@ -48,17 +48,25 @@ menu {
margin-top: .5rem;
padding-block: .25rem;
padding-inline: .8rem;
border: 1px solid var(--slate-400);
background: var(--white);
color: var(--foreground);
border: 1px solid var(--border);
background: var(--background);
border-radius: 100vmax;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
pointer-events: all;
transition-property: color, background-color, border-color, outline-color;
transition-duration: .15s;
transition-timing-function: var(--timing-function);
li {
display: flex;
align-items: center;
gap: .25ch;
svg {
fill: var(--foreground);
}
&:has(a:hover, a:focus-visible) {
svg.folder {
fill: var(--blue-500);
@@ -83,7 +91,7 @@ menu {
}
.icon {
width: 1.1em;
fill: var(--foreground);
}
.label {

View File

@@ -49,7 +49,7 @@ a {
&:hover,
&:focus-visible {
background-color: var(--slate-200);
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
}
&[aria-current="page"] {

View File

@@ -275,10 +275,11 @@ menu {
padding-block: .25rem;
top: calc(var(--mouse-y) - .5rem);
left: calc(var(--mouse-x) + .75rem);
background-color: var(--white);
border: 1px solid var(--slate-400);
color: var(--foreground);
background-color: var(--background);
border: 1px solid var(--border);
border-radius: .3rem;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px;
box-shadow: var(--shadow);
z-index: 1000;
transition-property: top, left;
transition-duration: .16s;
@@ -295,13 +296,17 @@ menu {
cursor: pointer;
width: 100%;
svg {
fill: var(--foreground);
}
&:hover,
&:focus-within {
color: var(--blue-700);
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: var(--blue-700);
fill: color-mix(in srgb, var(--accent) 75%, var(--background));
}
}
}
@@ -313,10 +318,11 @@ dialog {
max-width: calc(100% - 6rem);
padding: 1.5rem 2rem;
z-index: 9999;
background-color: var(--white);
border: 1px solid var(--slate-400);
color: var(--foreground);
background-color: var(--background);
border: 1px solid var(--border);
border-radius: .75rem;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px;
box-shadow: var(--shadow);
overflow: visible;
transform: translateY(1rem);
transition: visibility 0s ease-out .3s, opacity .3s ease-out, transform .3s ease-out;

View File

@@ -1,22 +1,36 @@
<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 { PopoverArrow, PopoverContent, PopoverRoot, PopoverTrigger } from 'radix-vue'
import { useStore } from '@nanostores/vue';
import {
PopoverArrow,
PopoverContent,
PopoverRoot,
PopoverTrigger,
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
DialogTrigger
} from 'radix-vue'
import { currentLang } from '@/i18n/store';
import { t } from '@/i18n/store';
import { useStore } from '@nanostores/vue'
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 buttonRef = ref<HTMLButtonElement | null>(null)
const wrapperHovered = useElementHover(wrapper, { delayEnter: 100, delayLeave: 500 })
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) => {
// 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(() => {
url = new URL(window.location.href)
@@ -80,8 +94,7 @@ const menus: Menu[] = [
{
name: 'Mines Blanches',
img: '/images/aldys-cantane-mines-blanches-cover.png',
url: `/${$currentLang.value}/aldys/cantane/mines-blanches`,
newItem: true
url: `/${$currentLang.value}/aldys/cantane/mines-blanches`
},
]
}
@@ -130,6 +143,26 @@ const menus: Menu[] = [
</li>
</menu>
</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>
</PopoverRoot>
</div>
@@ -151,6 +184,38 @@ const menus: Menu[] = [
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 {
margin-top: .25rem;
margin-bottom: .5rem;
@@ -189,7 +254,7 @@ const menus: Menu[] = [
img {
display: block;
border-radius: .3rem;
border: .1rem solid var(--white);
border: .1rem solid var(--background);
outline: .15rem solid transparent;
}
@@ -202,15 +267,15 @@ const menus: Menu[] = [
pointer-events: none;
.fill { fill: var(--orange-500); }
.stroke { fill: var(--white); }
.stroke { fill: var(--background); }
}
}
figcaption {
font-size: .75em;
text-align: center;
color: var(--slate-500);
color: color-mix(in srgb, var(--foreground) 80%, var(--background));
}
}
}
</style>
</style>

View File

@@ -308,11 +308,15 @@ function resetAllFields(actionAfter?: "focusAfter") {
margin-left: calc(45px + .5rem);
padding: .5rem 1.2rem;
width: calc(100% - 3rem);
background: var(--white);
border: 1px solid var(--slate-400);
color: var(--foreground);
background: var(--background);
border: 1px solid var(--border);
border-radius: 25px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
box-shadow: var(--shadow);
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) {
width: clamp(25rem, 35dvw, 30rem);
@@ -338,7 +342,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
flex-grow: 1;
&::placeholder {
color: var(--slate-400);
color: color-mix(in srgb, var(--foreground) 75%, var(--background));
}
}
@@ -348,7 +352,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
transform: translateY(-50%);
left: 0;
width: $search-icon-size;
fill: var(--slate-400);
fill: color-mix(in srgb, var(--foreground) 75%, var(--background));
pointer-events: none;
}
@@ -375,16 +379,20 @@ function resetAllFields(actionAfter?: "focusAfter") {
}
&:hover {
.icon {
color: var(--foreground);
}
&::before {
background-color: var(--slate-300);
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
}
}
&:focus-visible {
&::before {
background-color: var(--slate-200);
outline: 1px dotted var(--slate-500);
outline: 4px auto var(--slate-900);
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
}
}
}
@@ -399,22 +407,22 @@ function resetAllFields(actionAfter?: "focusAfter") {
&:hover {
&::before {
background-color: var(--slate-300);
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
}
}
&:focus-visible {
&::before {
background-color: var(--slate-200);
outline: 1px dotted var(--slate-500);
outline: 4px auto var(--slate-900);
background-color: color-mix(in srgb, var(--foreground) 20%, var(--background));
outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
}
}
}
.close-btn {
font-size: 1.1em;
color: var(--slate-500);
color: color-mix(in srgb, var(--foreground) 75%, var(--background));
}
}
@@ -436,7 +444,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
}
&::-webkit-scrollbar-thumb {
background-color: var(--slate-300);
background-color: color-mix(in srgb, var(--foreground) 30%, var(--background));
}
> * + * {
@@ -453,7 +461,7 @@ function resetAllFields(actionAfter?: "focusAfter") {
}
&:hover:not(:has(a:hover)) {
background-color: var(--slate-100);
background-color: color-mix(in srgb, var(--foreground) 10%, var(--background));
.title {
text-decoration: underline;
@@ -490,9 +498,14 @@ function resetAllFields(actionAfter?: "focusAfter") {
.title {
font-weight: 500;
text-underline-offset: 2px;
& + .desc {
margin-top: .25rem;
}
}
.desc {
color: var(--slate-500);
color: color-mix(in srgb, var(--foreground) 50%, var(--background));
.map-link {
margin-top: .5rem;
@@ -528,12 +541,12 @@ function resetAllFields(actionAfter?: "focusAfter") {
top: 50%;
transform: translateY(-50%);
right: .75rem;
fill: var(--slate-400);
fill: color-mix(in srgb, var(--foreground) 70%, var(--background));
}
&:focus-visible {
outline: 1px dotted var(--slate-500);
outline: 4px auto var(--slate-900);
outline: 1px dotted color-mix(in srgb, var(--foreground) 50%, var(--background));
outline: 4px auto color-mix(in srgb, var(--foreground) 90%, var(--background));
.title {
text-decoration: underline;
@@ -600,8 +613,9 @@ function resetAllFields(actionAfter?: "focusAfter") {
&[data-focused="true"] {
margin-bottom: 1em;
.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;
padding-bottom: .5rem;

View File

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

View File

@@ -35,7 +35,19 @@ export const translations: LanguageDict = {
'maps.markers.new': 'Nouveau marqueur',
'maps.markers.addPersonal': 'Ajouter un marqueur personnel',
'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 !'
'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': {
'lang.fr': 'Français',
@@ -67,6 +79,18 @@ export const translations: LanguageDict = {
'maps.markers.new': 'New marker',
'maps.markers.addPersonal': 'Add a personal marker',
'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 !'
'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;

View File

@@ -59,6 +59,54 @@ const lang = getLangFromUrl(Astro.url) || defaultLang;
<script is:inline type="text/javascript" src="/js/leaflet-measure.min.js"></script>
</head>
<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 />
</body>
</html>

View File

@@ -28,6 +28,7 @@ export interface SearchConfig {
}
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.