✨ feat(frontend): carte interactive desktop avec Konva.js
Story 3.6 : Carte interactive desktop (Konva.js) - Installation de konva et vue-konva - Configuration nuxt.config.ts pour transpile Konva - Création mapZones.ts avec 5 zones et connexions - Composant InteractiveMap.client.vue : - Canvas Konva avec zones cliquables - États visuels (visité/non visité/verrouillé) - Tooltip au hover avec statut - Marqueur de position animé - Navigation clavier (Tab + Enter) - Légende interactive - Traductions map.* FR/EN - Lazy-loading client-only (.client.vue) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Story 3.6: Carte interactive desktop (Konva.js)
|
# Story 3.6: Carte interactive desktop (Konva.js)
|
||||||
|
|
||||||
Status: ready-for-dev
|
Status: review
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ development_status:
|
|||||||
3-3-textes-narrateur-contextuels-arc-revelation: review
|
3-3-textes-narrateur-contextuels-arc-revelation: review
|
||||||
3-4-barre-progression-globale-xp-bar: review
|
3-4-barre-progression-globale-xp-bar: review
|
||||||
3-5-logique-progression-deblocage-contact: review
|
3-5-logique-progression-deblocage-contact: review
|
||||||
3-6-carte-interactive-desktop-konvajs: ready-for-dev
|
3-6-carte-interactive-desktop-konvajs: review
|
||||||
3-7-navigation-mobile-chemin-libre-bottom-bar: ready-for-dev
|
3-7-navigation-mobile-chemin-libre-bottom-bar: ready-for-dev
|
||||||
epic-3-retrospective: optional
|
epic-3-retrospective: optional
|
||||||
|
|
||||||
|
|||||||
326
frontend/app/components/feature/InteractiveMap.client.vue
Normal file
326
frontend/app/components/feature/InteractiveMap.client.vue
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Konva from 'konva'
|
||||||
|
import { mapZones, mapConnections, type MapZone } from '~/data/mapZones'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
currentSection?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { locale, t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const localePath = useLocalePath()
|
||||||
|
const progressionStore = useProgressionStore()
|
||||||
|
const reducedMotion = useReducedMotion()
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const stageRef = ref<Konva.Stage | null>(null)
|
||||||
|
const animationRef = ref<Konva.Animation | null>(null)
|
||||||
|
|
||||||
|
const CANVAS_WIDTH = 800
|
||||||
|
const CANVAS_HEIGHT = 400
|
||||||
|
|
||||||
|
const hoveredZone = ref<MapZone | null>(null)
|
||||||
|
const tooltipPosition = ref({ x: 0, y: 0 })
|
||||||
|
const focusedZoneIndex = ref(-1)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (containerRef.value) {
|
||||||
|
initCanvas()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationRef.value) {
|
||||||
|
animationRef.value.stop()
|
||||||
|
}
|
||||||
|
if (stageRef.value) {
|
||||||
|
stageRef.value.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function initCanvas() {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
const stage = new Konva.Stage({
|
||||||
|
container: containerRef.value,
|
||||||
|
width: CANVAS_WIDTH,
|
||||||
|
height: CANVAS_HEIGHT,
|
||||||
|
})
|
||||||
|
|
||||||
|
stageRef.value = stage
|
||||||
|
|
||||||
|
const backgroundLayer = new Konva.Layer()
|
||||||
|
drawBackground(backgroundLayer)
|
||||||
|
stage.add(backgroundLayer)
|
||||||
|
|
||||||
|
const zonesLayer = new Konva.Layer()
|
||||||
|
drawZones(zonesLayer)
|
||||||
|
stage.add(zonesLayer)
|
||||||
|
|
||||||
|
if (props.currentSection && !reducedMotion.value) {
|
||||||
|
const markerLayer = new Konva.Layer()
|
||||||
|
drawPositionMarker(markerLayer)
|
||||||
|
stage.add(markerLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBackground(layer: Konva.Layer) {
|
||||||
|
const background = new Konva.Rect({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: CANVAS_WIDTH,
|
||||||
|
height: CANVAS_HEIGHT,
|
||||||
|
fill: '#0f172a',
|
||||||
|
})
|
||||||
|
layer.add(background)
|
||||||
|
|
||||||
|
mapConnections.forEach(([from, to]) => {
|
||||||
|
const zoneFrom = mapZones.find((z) => z.id === from)
|
||||||
|
const zoneTo = mapZones.find((z) => z.id === to)
|
||||||
|
if (zoneFrom && zoneTo) {
|
||||||
|
const line = new Konva.Line({
|
||||||
|
points: [
|
||||||
|
zoneFrom.position.x,
|
||||||
|
zoneFrom.position.y,
|
||||||
|
zoneTo.position.x,
|
||||||
|
zoneTo.position.y,
|
||||||
|
],
|
||||||
|
stroke: '#334155',
|
||||||
|
strokeWidth: 2,
|
||||||
|
dash: [10, 5],
|
||||||
|
opacity: 0.5,
|
||||||
|
})
|
||||||
|
layer.add(line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawZones(layer: Konva.Layer) {
|
||||||
|
mapZones.forEach((zone) => {
|
||||||
|
const isVisited =
|
||||||
|
zone.id !== 'contact' && progressionStore.visitedSections.includes(zone.id)
|
||||||
|
const isLocked = zone.id === 'contact' && !progressionStore.contactUnlocked
|
||||||
|
|
||||||
|
const group = new Konva.Group({
|
||||||
|
x: zone.position.x,
|
||||||
|
y: zone.position.y,
|
||||||
|
})
|
||||||
|
|
||||||
|
const circle = new Konva.Circle({
|
||||||
|
radius: zone.size,
|
||||||
|
fill: isLocked ? '#475569' : zone.color,
|
||||||
|
opacity: isVisited ? 1 : 0.6,
|
||||||
|
shadowColor: zone.color,
|
||||||
|
shadowBlur: isVisited ? 20 : 0,
|
||||||
|
shadowOpacity: 0.5,
|
||||||
|
})
|
||||||
|
group.add(circle)
|
||||||
|
|
||||||
|
const icon = new Konva.Text({
|
||||||
|
text: isLocked ? '🔒' : zone.emoji,
|
||||||
|
fontSize: 32,
|
||||||
|
x: -16,
|
||||||
|
y: -16,
|
||||||
|
})
|
||||||
|
group.add(icon)
|
||||||
|
|
||||||
|
if (isVisited && zone.id !== 'contact') {
|
||||||
|
const check = new Konva.Text({
|
||||||
|
text: '✓',
|
||||||
|
fontSize: 24,
|
||||||
|
fill: '#22c55e',
|
||||||
|
x: zone.size - 25,
|
||||||
|
y: -zone.size - 5,
|
||||||
|
})
|
||||||
|
group.add(check)
|
||||||
|
}
|
||||||
|
|
||||||
|
group.on('mouseenter', () => {
|
||||||
|
document.body.style.cursor = 'pointer'
|
||||||
|
hoveredZone.value = zone
|
||||||
|
tooltipPosition.value = {
|
||||||
|
x: zone.position.x,
|
||||||
|
y: zone.position.y - zone.size - 20,
|
||||||
|
}
|
||||||
|
circle.shadowBlur(30)
|
||||||
|
layer.draw()
|
||||||
|
})
|
||||||
|
|
||||||
|
group.on('mouseleave', () => {
|
||||||
|
document.body.style.cursor = 'default'
|
||||||
|
hoveredZone.value = null
|
||||||
|
circle.shadowBlur(isVisited ? 20 : 0)
|
||||||
|
layer.draw()
|
||||||
|
})
|
||||||
|
|
||||||
|
group.on('click tap', () => {
|
||||||
|
handleZoneClick(zone)
|
||||||
|
})
|
||||||
|
|
||||||
|
layer.add(group)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPositionMarker(layer: Konva.Layer) {
|
||||||
|
const currentZone = mapZones.find((z) => z.id === props.currentSection)
|
||||||
|
if (!currentZone) return
|
||||||
|
|
||||||
|
const marker = new Konva.Circle({
|
||||||
|
x: currentZone.position.x,
|
||||||
|
y: currentZone.position.y,
|
||||||
|
radius: 10,
|
||||||
|
fill: '#fa784f',
|
||||||
|
opacity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const anim = new Konva.Animation((frame) => {
|
||||||
|
if (!frame) return
|
||||||
|
const scale = 1 + 0.3 * Math.sin(frame.time / 200)
|
||||||
|
marker.scale({ x: scale, y: scale })
|
||||||
|
marker.opacity(1 - 0.3 * Math.abs(Math.sin(frame.time / 200)))
|
||||||
|
}, layer)
|
||||||
|
|
||||||
|
animationRef.value = anim
|
||||||
|
anim.start()
|
||||||
|
layer.add(marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleZoneClick(zone: MapZone) {
|
||||||
|
if (zone.id === 'contact' && !progressionStore.contactUnlocked) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = locale.value === 'fr' ? zone.route.fr : zone.route.en
|
||||||
|
router.push(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.shiftKey) {
|
||||||
|
focusedZoneIndex.value =
|
||||||
|
(focusedZoneIndex.value - 1 + mapZones.length) % mapZones.length
|
||||||
|
} else {
|
||||||
|
focusedZoneIndex.value = (focusedZoneIndex.value + 1) % mapZones.length
|
||||||
|
}
|
||||||
|
highlightFocusedZone()
|
||||||
|
} else if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
if (focusedZoneIndex.value >= 0) {
|
||||||
|
handleZoneClick(mapZones[focusedZoneIndex.value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightFocusedZone() {
|
||||||
|
const zone = mapZones[focusedZoneIndex.value]
|
||||||
|
hoveredZone.value = zone
|
||||||
|
tooltipPosition.value = {
|
||||||
|
x: zone.position.x,
|
||||||
|
y: zone.position.y - zone.size - 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getZoneStatus(zone: MapZone): string {
|
||||||
|
if (zone.id === 'contact' && !progressionStore.contactUnlocked) {
|
||||||
|
return t('map.locked')
|
||||||
|
}
|
||||||
|
if (zone.id !== 'contact' && progressionStore.visitedSections.includes(zone.id)) {
|
||||||
|
return t('map.visited')
|
||||||
|
}
|
||||||
|
return t('map.clickToExplore')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="interactive-map-container relative"
|
||||||
|
tabindex="0"
|
||||||
|
role="application"
|
||||||
|
:aria-label="t('map.ariaLabel')"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
class="konva-container rounded-xl overflow-hidden shadow-2xl border border-sky-dark-100"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="hoveredZone"
|
||||||
|
class="tooltip absolute pointer-events-none z-10 bg-sky-dark-50 border border-sky-dark-100 rounded-lg px-3 py-2 shadow-lg"
|
||||||
|
:style="{
|
||||||
|
left: `${tooltipPosition.x}px`,
|
||||||
|
top: `${tooltipPosition.y}px`,
|
||||||
|
transform: 'translate(-50%, -100%)',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<p class="font-ui font-semibold text-sky-text">
|
||||||
|
{{ locale === 'fr' ? hoveredZone.label.fr : hoveredZone.label.en }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-sky-text-muted">
|
||||||
|
{{ getZoneStatus(hoveredZone) }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-sky-dark-50 border-r border-b border-sky-dark-100 transform rotate-45"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="legend absolute bottom-4 left-4 bg-sky-dark-50/80 backdrop-blur rounded-lg p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4 text-xs font-ui">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="w-3 h-3 rounded-full bg-sky-accent opacity-60" />
|
||||||
|
<span class="text-sky-text-muted">{{ t('map.legend.notVisited') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
class="w-3 h-3 rounded-full bg-sky-accent shadow-lg"
|
||||||
|
style="box-shadow: 0 0 10px var(--sky-accent)"
|
||||||
|
/>
|
||||||
|
<span class="text-sky-text-muted">{{ t('map.legend.visited') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="w-3 h-3 rounded-full bg-gray-500" />
|
||||||
|
<span class="text-sky-text-muted">{{ t('map.legend.locked') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="sr-only">
|
||||||
|
{{ t('map.instructions') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.interactive-map-container {
|
||||||
|
width: 800px;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactive-map-container:focus {
|
||||||
|
outline: 2px solid var(--sky-accent);
|
||||||
|
outline-offset: 4px;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
frontend/app/data/mapZones.ts
Normal file
71
frontend/app/data/mapZones.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export interface MapZone {
|
||||||
|
id: 'projets' | 'competences' | 'temoignages' | 'parcours' | 'contact'
|
||||||
|
label: {
|
||||||
|
fr: string
|
||||||
|
en: string
|
||||||
|
}
|
||||||
|
route: {
|
||||||
|
fr: string
|
||||||
|
en: string
|
||||||
|
}
|
||||||
|
position: { x: number; y: number }
|
||||||
|
color: string
|
||||||
|
emoji: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapZones: MapZone[] = [
|
||||||
|
{
|
||||||
|
id: 'projets',
|
||||||
|
label: { fr: 'Projets', en: 'Projects' },
|
||||||
|
route: { fr: '/projets', en: '/en/projects' },
|
||||||
|
position: { x: 150, y: 180 },
|
||||||
|
color: '#3b82f6',
|
||||||
|
emoji: '💻',
|
||||||
|
size: 70,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'competences',
|
||||||
|
label: { fr: 'Compétences', en: 'Skills' },
|
||||||
|
route: { fr: '/competences', en: '/en/skills' },
|
||||||
|
position: { x: 350, y: 100 },
|
||||||
|
color: '#10b981',
|
||||||
|
emoji: '⚡',
|
||||||
|
size: 70,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'temoignages',
|
||||||
|
label: { fr: 'Témoignages', en: 'Testimonials' },
|
||||||
|
route: { fr: '/temoignages', en: '/en/testimonials' },
|
||||||
|
position: { x: 300, y: 280 },
|
||||||
|
color: '#f59e0b',
|
||||||
|
emoji: '💬',
|
||||||
|
size: 70,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'parcours',
|
||||||
|
label: { fr: 'Parcours', en: 'Journey' },
|
||||||
|
route: { fr: '/parcours', en: '/en/journey' },
|
||||||
|
position: { x: 520, y: 220 },
|
||||||
|
color: '#8b5cf6',
|
||||||
|
emoji: '📍',
|
||||||
|
size: 70,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'contact',
|
||||||
|
label: { fr: 'Contact', en: 'Contact' },
|
||||||
|
route: { fr: '/contact', en: '/en/contact' },
|
||||||
|
position: { x: 650, y: 150 },
|
||||||
|
color: '#fa784f',
|
||||||
|
emoji: '📧',
|
||||||
|
size: 70,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const mapConnections = [
|
||||||
|
['projets', 'competences'],
|
||||||
|
['competences', 'temoignages'],
|
||||||
|
['temoignages', 'parcours'],
|
||||||
|
['parcours', 'contact'],
|
||||||
|
['projets', 'temoignages'],
|
||||||
|
]
|
||||||
@@ -179,6 +179,18 @@
|
|||||||
"clickToSkip": "Click or press Space to skip",
|
"clickToSkip": "Click or press Space to skip",
|
||||||
"bugAlt": "The Bug - Stage {stage}"
|
"bugAlt": "The Bug - Stage {stage}"
|
||||||
},
|
},
|
||||||
|
"map": {
|
||||||
|
"ariaLabel": "Interactive portfolio map. Use Tab to navigate between zones and Enter to explore.",
|
||||||
|
"instructions": "Use Tab keys to navigate between zones and Enter or Space to explore a zone.",
|
||||||
|
"locked": "Locked zone - Explore more",
|
||||||
|
"visited": "Already visited",
|
||||||
|
"clickToExplore": "Click to explore",
|
||||||
|
"legend": {
|
||||||
|
"notVisited": "Not visited",
|
||||||
|
"visited": "Visited",
|
||||||
|
"locked": "Locked"
|
||||||
|
}
|
||||||
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"label": "Progress: {percent}%",
|
"label": "Progress: {percent}%",
|
||||||
"title": "Portfolio exploration",
|
"title": "Portfolio exploration",
|
||||||
|
|||||||
@@ -179,6 +179,18 @@
|
|||||||
"clickToSkip": "Cliquez ou appuyez sur Espace pour passer",
|
"clickToSkip": "Cliquez ou appuyez sur Espace pour passer",
|
||||||
"bugAlt": "Le Bug - Stade {stage}"
|
"bugAlt": "Le Bug - Stade {stage}"
|
||||||
},
|
},
|
||||||
|
"map": {
|
||||||
|
"ariaLabel": "Carte interactive du portfolio. Utilisez Tab pour naviguer entre les zones et Entrée pour explorer.",
|
||||||
|
"instructions": "Utilisez les touches Tab pour naviguer entre les zones et Entrée ou Espace pour explorer une zone.",
|
||||||
|
"locked": "Zone verrouillée - Explorez davantage",
|
||||||
|
"visited": "Déjà visité",
|
||||||
|
"clickToExplore": "Cliquez pour explorer",
|
||||||
|
"legend": {
|
||||||
|
"notVisited": "Non visité",
|
||||||
|
"visited": "Visité",
|
||||||
|
"locked": "Verrouillé"
|
||||||
|
}
|
||||||
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"label": "Progression : {percent}%",
|
"label": "Progression : {percent}%",
|
||||||
"title": "Exploration du portfolio",
|
"title": "Exploration du portfolio",
|
||||||
|
|||||||
@@ -67,4 +67,8 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
compatibilityDate: '2025-01-01',
|
compatibilityDate: '2025-01-01',
|
||||||
|
|
||||||
|
build: {
|
||||||
|
transpile: ['konva', 'vue-konva'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
50
frontend/package-lock.json
generated
50
frontend/package-lock.json
generated
@@ -12,8 +12,10 @@
|
|||||||
"@nuxtjs/i18n": "^9.0.0",
|
"@nuxtjs/i18n": "^9.0.0",
|
||||||
"@nuxtjs/sitemap": "^7.2.0",
|
"@nuxtjs/sitemap": "^7.2.0",
|
||||||
"@pinia/nuxt": "^0.9.0",
|
"@pinia/nuxt": "^0.9.0",
|
||||||
|
"konva": "^10.2.0",
|
||||||
"nuxt": "^3.16.0",
|
"nuxt": "^3.16.0",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0"
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
|
"vue-konva": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.4.24",
|
"autoprefixer": "^10.4.24",
|
||||||
@@ -6643,6 +6645,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz",
|
||||||
"integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="
|
"integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/konva": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/konva/-/konva-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-JBoz0Xjbf49UPxCZegZ4WseqOzJ+C4AUDOtJ9eBve5RS5Fcq/u8TdBD5fDl/uPFInpC3a9uycm0sRyZpF4hheg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/lavrton"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/konva"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/launch-editor": {
|
"node_modules/launch-editor": {
|
||||||
"version": "2.12.0",
|
"version": "2.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz",
|
||||||
@@ -11664,6 +11685,33 @@
|
|||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-konva": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-konva/-/vue-konva-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-z7yB4VSyYaBAfRRlNCaxINp3+h0lpy+C9wm+FeQ0rBZylGRBMbNOOQbURjNdsS/uo3togcWz8jrM+0TVTDUTeQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/lavrton"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/konva"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"konva": ">7",
|
||||||
|
"vue": "^3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.6.4",
|
"version": "4.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
"@nuxtjs/i18n": "^9.0.0",
|
"@nuxtjs/i18n": "^9.0.0",
|
||||||
"@nuxtjs/sitemap": "^7.2.0",
|
"@nuxtjs/sitemap": "^7.2.0",
|
||||||
"@pinia/nuxt": "^0.9.0",
|
"@pinia/nuxt": "^0.9.0",
|
||||||
|
"konva": "^10.2.0",
|
||||||
"nuxt": "^3.16.0",
|
"nuxt": "^3.16.0",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0"
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
|
"vue-konva": "^3.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.4.24",
|
"autoprefixer": "^10.4.24",
|
||||||
|
|||||||
Reference in New Issue
Block a user