Cours Vue.js · Module 9: Composables et VueUse
VueUse - La bibliothèque de composables
Dans cette leçon8
VueUse est une collection de plus de 200 composables prêts à l'emploi - comme la bibliothèque des protocoles de recherche standards de NOVA LAB ! Pourquoi une telle bibliothèque ? Chaque projet ramène les mêmes tâches : mémoriser des réglages dans localStorage, suivre la taille de la fenêtre, la position de la souris ou le mode sombre, retarder une recherche jusqu'à ce que l'utilisateur arrête de taper. Chacune demande d'écouter des événements du navigateur et de faire le ménage ensuite. Tu peux tout réécrire à chaque projet, ou utiliser des fonctions éprouvées et testées du paquet @vueuse/core. Seules les fonctions que tu importes vraiment finissent dans le bundle de l'application.
Installer VueUse
Tu installes la bibliothèque comme n'importe quel paquet npm. La version actuelle de VueUse exige Vue 3.5 ou plus récent :
1npm install @vueuse/coreAprès l'installation, tu importes des fonctions individuelles depuis '@vueuse/core', exactement comme ref ou computed depuis 'vue'. Aucun enregistrement de plugin n'est nécessaire.
useLocalStorage
Le premier protocole enregistre les réglages du laboratoire pour qu'ils survivent à un rafraîchissement de la page. useLocalStorage prend une clé et une valeur par défaut, et retourne une ref synchronisée avec le localStorage du navigateur :
1<script setup>
2import { useLocalStorage } from '@vueuse/core'
3
4// Se synchronise automatiquement avec localStorage
5const labSettings = useLocalStorage('lab-settings', {
6 mode: 'exploration',
7 sensorsPerView: 12,
8 sortBy: 'temperature'
9})
10
11// Les changements sont automatiquement enregistrés dans localStorage
12labSettings.value.mode = 'research'
13</script>
14
15<template>
16 <div>
17 <p>Mode : {{ labSettings.mode }}</p>
18 <button @click="labSettings.mode = 'maintenance'">
19 Changer de mode
20 </button>
21 </div>
22</template>La valeur par défaut n'est utilisée que si rien n'est encore stocké sous la clé lab-settings. Les objets sont enregistrés en JSON, et la modification d'un seul champ est enregistrée elle aussi, car VueUse observe l'objet en profondeur.
useMouseInElement
Ce composable suit la souris par rapport à un élément précis. Il retourne entre autres elementX et elementY, la position mesurée depuis le coin supérieur gauche de l'élément, et isOutside, qui indique si le curseur se trouve en dehors. Les simples x et y sont les coordonnées sur toute la page :
1<script setup>
2import { ref } from 'vue'
3import { useMouseInElement } from '@vueuse/core'
4
5const sensorPanel = ref(null)
6const { elementX, elementY, isOutside } = useMouseInElement(sensorPanel)
7</script>
8
9<template>
10 <div ref="sensorPanel" class="sensor-panel">
11 <img src="/sensor-display.jpg" />
12 <div v-if="!isOutside" class="tooltip" :style="{ left: elementX + 'px', top: elementY + 'px' }">
13 Position : {{ elementX }}, {{ elementY }}
14 </div>
15 </div>
16</template>La ref sensorPanel se relie à l'élément via l'attribut ref ; depuis Vue 3.5, tu peux aussi utiliser useTemplateRef('sensorPanel'). Pour que l'infobulle suive le curseur, le panneau a besoin de position: relative en CSS et l'infobulle de position: absolute.
useIntersectionObserver
Le protocole suivant vérifie si un élément est visible dans la fenêtre du navigateur. C'est la base du chargement paresseux des images et des animations déclenchées au défilement. Le callback reçoit une liste d'entrées observées, et le champ isIntersecting indique si l'élément est entré dans la zone visible :
1<script setup>
2import { ref } from 'vue'
3import { useIntersectionObserver } from '@vueuse/core'
4
5const experimentPanel = ref(null)
6const isVisible = ref(false)
7
8useIntersectionObserver(
9 experimentPanel,
10 ([{ isIntersecting }]) => {
11 isVisible.value = isIntersecting
12 }
13)
14</script>
15
16<template>
17 <div ref="experimentPanel">
18 <Transition name="fade">
19 <img v-if="isVisible" src="/experiment-data.jpg" />
20 </Transition>
21 </div>
22</template>L'image n'est téléchargée que lorsque le panneau apparaît à l'écran. VueUse déconnecte lui-même l'observateur au démontage du composant, donc tu n'as pas à écrire ton propre nettoyage.
useWindowSize
useWindowSize retourne les width et height réactifs de la fenêtre, qui se mettent à jour à chaque redimensionnement. À partir d'eux, il est facile de calculer des seuils d'affichage avec computed :
1<script setup>
2import { useWindowSize } from '@vueuse/core'
3import { computed } from 'vue'
4
5const { width, height } = useWindowSize()
6
7const isMobile = computed(() => width.value < 768)
8const isTablet = computed(() => width.value >= 768 && width.value < 1024)
9const isDesktop = computed(() => width.value >= 1024)
10</script>
11
12<template>
13 <div>
14 <p v-if="isMobile">Vue mobile</p>
15 <p v-else-if="isTablet">Vue tablette</p>
16 <p v-else>Vue bureau</p>
17 <p>Dimensions : {{ width }} x {{ height }}</p>
18 </div>
19</template>Le template change de message sans aucun écouteur resize manuel. Si tu as plus de seuils, VueUse propose pour cela le composable spécialisé useBreakpoints.
useDark & useToggle
useDark retourne une ref contenant la valeur booléenne du mode sombre. Par défaut, il part du réglage du système, ajoute la classe dark à l'élément <html> et mémorise le choix dans localStorage. useToggle transforme une telle ref en fonction de bascule :
1<script setup>
2import { useDark, useToggle } from '@vueuse/core'
3
4const isDark = useDark()
5const toggleDark = useToggle(isDark)
6</script>
7
8<template>
9 <div :class="{ dark: isDark }">
10 <button @click="toggleDark()">
11 Thème {{ isDark ? 'clair' : 'sombre' }}
12 </button>
13 </div>
14</template>
15
16<style>
17body {
18 transition: background 0.3s, color 0.3s;
19}
20
21.dark {
22 background: #1a1a1a;
23 color: #f5f5f5;
24}
25</style>Après un clic, la classe dark apparaît sur <html> et sur le div qui porte la liaison :class. Cette liaison n'est pas nécessaire, car useDark ajoute lui-même la classe sur <html> ; on la garde pour montrer d'où vient le style.
refDebounced et refThrottled
Le debounce attend qu'une valeur cesse de changer pendant un temps donné, tandis que le throttle laisse passer un changement au plus une fois par intervalle. Dans VueUse, c'est le rôle de refDebounced et refThrottled, qui retournent une copie retardée d'une ref. Les anciens noms useDebounce et useThrottle fonctionnent encore, mais ils sont marqués comme obsolètes :
1<script setup>
2import { ref } from 'vue'
3import { refDebounced, refThrottled } from '@vueuse/core'
4
5// Debounce - attend que l'utilisateur arrête de taper
6const searchQuery = ref('')
7const debouncedQuery = refDebounced(searchQuery, 500)
8
9// Throttle - au maximum une fois toutes les X ms
10const scrollPosition = ref(0)
11const throttledScroll = refThrottled(scrollPosition, 100)
12</script>
13
14<template>
15 <input v-model="searchQuery" placeholder="Rechercher..." />
16 <p>Recherche : {{ debouncedQuery }}</p>
17</template>Quand tu tapes "oxy", debouncedQuery ne change qu'une demi-seconde après la dernière touche, donc la requête part vers le serveur une seule fois au lieu de trois. Tu mettrais à jour scrollPosition dans un gestionnaire de défilement. Pour retarder des appels de fonctions, VueUse propose aussi useDebounceFn et useThrottleFn.
Composables populaires de VueUse
Pour finir, un aide-mémoire des fonctions que tu utiliseras le plus souvent :
| Composable | Description |
|---|---|
useLocalStorage | localStorage réactif |
useFetch | Fetch avec réactivité |
useIntersectionObserver | Détection de visibilité |
useMouseInElement | Position de la souris dans un élément |
useWindowSize | Taille de la fenêtre |
useDark | Mode sombre |
refDebounced | Réactivité retardée (debounce) |
useClipboard | Copie dans le presse-papiers |
Mon conseil : avant d'écrire ton propre composable pour une fonctionnalité du navigateur, jette un œil à vueuse.org. Si la fonction y est déjà, tu obtiens un code avec des tests et un nettoyage correct. Dans l'éditeur ci-dessous, tu verras à quoi ressemble un tel composable de l'intérieur : useConnectionMonitor démarre lui-même ses minuteurs dans onMounted et les nettoie dans onUnmounted - exactement le travail que VueUse fait à ta place.
Retiens : VueUse est un entrepôt de protocoles de station prêts à l'emploi - avant de construire un nouvel instrument, vérifie s'il n'est pas déjà sur l'étagère.
Code de cette leçon : App.vue
1<script setup>
2import { ref, onMounted, onUnmounted } from 'vue'
3
4// Composable avec hooks de cycle de vie : useConnectionMonitor
5function useConnectionMonitor(serverUrl) {
6 const isConnected = ref(false)
7 const connectionTime = ref(null)
8 const disconnectionTime = ref(null)
9 const reconnectAttempts = ref(0)
10 const latency = ref(0)
11 const messages = ref([])
12
13 let intervalId = null
14 let connectionCheckId = null
15
16 function addMessage(type, text) {
17 messages.value.unshift({
18 id: Date.now(),
19 type,
20 text,
21 timestamp: new Date().toLocaleTimeString()
22 })
23 if (messages.value.length > 10) {
24 messages.value.pop()
25 }
26 }
27
28 function connect() {
29 isConnected.value = true
30 connectionTime.value = new Date()
31 reconnectAttempts.value = 0
32 addMessage('success', `Connected to ${serverUrl}`)
33
34 // Simule la surveillance de la latence
35 intervalId = setInterval(() => {
36 latency.value = Math.floor(Math.random() * 100) + 20
37 }, 2000)
38 }
39
40 function disconnect() {
41 isConnected.value = false
42 disconnectionTime.value = new Date()
43 addMessage('warning', `Disconnected from ${serverUrl}`)
44
45 if (intervalId) {
46 clearInterval(intervalId)
47 intervalId = null
48 }
49 }
50
51 function attemptReconnect() {
52 if (isConnected.value) return
53
54 reconnectAttempts.value++
55 addMessage('info', `Reconnection attempt #${reconnectAttempts.value}`)
56
57 setTimeout(() => {
58 if (reconnectAttempts.value <= 3) {
59 connect()
60 addMessage('success', 'Reconnected successfully!')
61 } else {
62 addMessage('error', 'Max reconnection attempts reached')
63 }
64 }, 1500)
65 }
66
67 // Cycle de vie : montage
68 onMounted(() => {
69 addMessage('info', 'Connection monitor initialized')
70
71 // Connexion automatique au montage
72 setTimeout(() => {
73 connect()
74 }, 1000)
75
76 // Simule des déconnexions aléatoires
77 connectionCheckId = setInterval(() => {
78 if (isConnected.value && Math.random() < 0.1) {
79 addMessage('warning', 'Connection lost - random event')
80 disconnect()
81 setTimeout(attemptReconnect, 2000)
82 }
83 }, 5000)
84 })
85
86 // Cycle de vie : démontage
87 onUnmounted(() => {
88 addMessage('info', 'Connection monitor shutting down')
89 if (intervalId) clearInterval(intervalId)
90 if (connectionCheckId) clearInterval(connectionCheckId)
91 disconnect()
92 })
93
94 return {
95 isConnected,
96 connectionTime,
97 disconnectionTime,
98 reconnectAttempts,
99 latency,
100 messages,
101 connect,
102 disconnect,
103 attemptReconnect
104 }
105}
106
107// Crée des moniteurs de connexion pour différents serveurs
108const earthConnection = useConnectionMonitor('Earth Command Center')
109const marsConnection = useConnectionMonitor('Mars Base Alpha')
110const satelliteConnection = useConnectionMonitor('Orbital Satellite')
111</script>
112
113<template>
114 <div class="connection-lab">
115 <div class="header">
116 <h1>Moniteur de connexion - NOVA LAB</h1>
117 <p class="subtitle">Module 3 : composables avec hooks de cycle de vie</p>
118 </div>
119
120 <div class="connections-grid">
121 <!-- Connexion Terre -->
122 <div class="connection-card" :class="{ connected: earthConnection.isConnected }">
123 <div class="connection-header">
124 <h3>Earth Command</h3>
125 <div class="status-indicator" :class="{ active: earthConnection.isConnected }">
126 {{ earthConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
127 </div>
128 </div>
129
130 <div class="connection-stats">
131 <div class="stat-item">
132 <span class="stat-label">Latence :</span>
133 <span class="stat-value" :class="{ good: earthConnection.latency < 50 }">
134 {{ earthConnection.latency }}ms
135 </span>
136 </div>
137 <div class="stat-item">
138 <span class="stat-label">Tentatives de reconnexion :</span>
139 <span class="stat-value">{{ earthConnection.reconnectAttempts }}</span>
140 </div>
141 </div>
142
143 <div class="connection-times">
144 <div v-if="earthConnection.connectionTime" class="time-info">
145 Connecté : {{ earthConnection.connectionTime.toLocaleTimeString() }}
146 </div>
147 <div v-if="earthConnection.disconnectionTime" class="time-info">
148 Déconnecté : {{ earthConnection.disconnectionTime.toLocaleTimeString() }}
149 </div>
150 </div>
151
152 <div class="connection-controls">
153 <button
154 @click="earthConnection.connect"
155 :disabled="earthConnection.isConnected"
156 class="btn btn-success"
157 >
158 Connecter
159 </button>
160 <button
161 @click="earthConnection.disconnect"
162 :disabled="!earthConnection.isConnected"
163 class="btn btn-danger"
164 >
165 Déconnecter
166 </button>
167 <button
168 @click="earthConnection.attemptReconnect"
169 :disabled="earthConnection.isConnected"
170 class="btn btn-warning"
171 >
172 Reconnecter
173 </button>
174 </div>
175
176 <div class="messages-log">
177 <div
178 v-for="msg in earthConnection.messages.slice(0, 5)"
179 :key="msg.id"
180 class="log-message"
181 :class="'msg-' + msg.type"
182 >
183 <span class="msg-time">{{ msg.timestamp }}</span>
184 <span class="msg-text">{{ msg.text }}</span>
185 </div>
186 </div>
187 </div>
188
189 <!-- Mars Connection -->
190 <div class="connection-card" :class="{ connected: marsConnection.isConnected }">
191 <div class="connection-header">
192 <h3>Mars Base Alpha</h3>
193 <div class="status-indicator" :class="{ active: marsConnection.isConnected }">
194 {{ marsConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
195 </div>
196 </div>
197
198 <div class="connection-stats">
199 <div class="stat-item">
200 <span class="stat-label">Latence :</span>
201 <span class="stat-value" :class="{ good: marsConnection.latency < 50 }">
202 {{ marsConnection.latency }}ms
203 </span>
204 </div>
205 <div class="stat-item">
206 <span class="stat-label">Tentatives de reconnexion :</span>
207 <span class="stat-value">{{ marsConnection.reconnectAttempts }}</span>
208 </div>
209 </div>
210
211 <div class="connection-times">
212 <div v-if="marsConnection.connectionTime" class="time-info">
213 Connecté : {{ marsConnection.connectionTime.toLocaleTimeString() }}
214 </div>
215 <div v-if="marsConnection.disconnectionTime" class="time-info">
216 Déconnecté : {{ marsConnection.disconnectionTime.toLocaleTimeString() }}
217 </div>
218 </div>
219
220 <div class="connection-controls">
221 <button
222 @click="marsConnection.connect"
223 :disabled="marsConnection.isConnected"
224 class="btn btn-success"
225 >
226 Connecter
227 </button>
228 <button
229 @click="marsConnection.disconnect"
230 :disabled="!marsConnection.isConnected"
231 class="btn btn-danger"
232 >
233 Déconnecter
234 </button>
235 <button
236 @click="marsConnection.attemptReconnect"
237 :disabled="marsConnection.isConnected"
238 class="btn btn-warning"
239 >
240 Reconnecter
241 </button>
242 </div>
243
244 <div class="messages-log">
245 <div
246 v-for="msg in marsConnection.messages.slice(0, 5)"
247 :key="msg.id"
248 class="log-message"
249 :class="'msg-' + msg.type"
250 >
251 <span class="msg-time">{{ msg.timestamp }}</span>
252 <span class="msg-text">{{ msg.text }}</span>
253 </div>
254 </div>
255 </div>
256
257 <!-- Satellite Connection -->
258 <div class="connection-card" :class="{ connected: satelliteConnection.isConnected }">
259 <div class="connection-header">
260 <h3>Orbital Satellite</h3>
261 <div class="status-indicator" :class="{ active: satelliteConnection.isConnected }">
262 {{ satelliteConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
263 </div>
264 </div>
265
266 <div class="connection-stats">
267 <div class="stat-item">
268 <span class="stat-label">Latence :</span>
269 <span class="stat-value" :class="{ good: satelliteConnection.latency < 50 }">
270 {{ satelliteConnection.latency }}ms
271 </span>
272 </div>
273 <div class="stat-item">
274 <span class="stat-label">Tentatives de reconnexion :</span>
275 <span class="stat-value">{{ satelliteConnection.reconnectAttempts }}</span>
276 </div>
277 </div>
278
279 <div class="connection-times">
280 <div v-if="satelliteConnection.connectionTime" class="time-info">
281 Connecté : {{ satelliteConnection.connectionTime.toLocaleTimeString() }}
282 </div>
283 <div v-if="satelliteConnection.disconnectionTime" class="time-info">
284 Déconnecté : {{ satelliteConnection.disconnectionTime.toLocaleTimeString() }}
285 </div>
286 </div>
287
288 <div class="connection-controls">
289 <button
290 @click="satelliteConnection.connect"
291 :disabled="satelliteConnection.isConnected"
292 class="btn btn-success"
293 >
294 Connecter
295 </button>
296 <button
297 @click="satelliteConnection.disconnect"
298 :disabled="!satelliteConnection.isConnected"
299 class="btn btn-danger"
300 >
301 Déconnecter
302 </button>
303 <button
304 @click="satelliteConnection.attemptReconnect"
305 :disabled="satelliteConnection.isConnected"
306 class="btn btn-warning"
307 >
308 Reconnecter
309 </button>
310 </div>
311
312 <div class="messages-log">
313 <div
314 v-for="msg in satelliteConnection.messages.slice(0, 5)"
315 :key="msg.id"
316 class="log-message"
317 :class="'msg-' + msg.type"
318 >
319 <span class="msg-time">{{ msg.timestamp }}</span>
320 <span class="msg-text">{{ msg.text }}</span>
321 </div>
322 </div>
323 </div>
324 </div>
325
326 <div class="info-panel">
327 <h3>Hooks de cycle de vie dans les composables</h3>
328 <div class="info-content">
329 <p><strong>useConnectionMonitor()</strong> montre comment utiliser les hooks de cycle de vie :</p>
330 <ul>
331 <li><strong>onMounted()</strong> - Connexion automatique et initialisation de la surveillance</li>
332 <li><strong>onUnmounted()</strong> - Nettoyage des intervalles et déconnexion</li>
333 <li>Chaque instance de composable a ses propres hooks de cycle de vie</li>
334 <li>Gestion automatique des ressources (timers, connexions)</li>
335 <li>Prévention des fuites mémoire grâce au nettoyage dans onUnmounted</li>
336 </ul>
337 <div class="code-example">
338 <code>
339function useConnectionMonitor(url) {
340 onMounted(() => {
341 // Setup : connexion automatique, démarrage de la surveillance
342 })
343
344 onUnmounted(() => {
345 // Cleanup : nettoyage des timers, déconnexion
346 })
347
348 return { isConnected, connect, disconnect }
349}
350 </code>
351 </div>
352 </div>
353 </div>
354 </div>
355</template>
356
357<style scoped>
358.connection-lab {
359 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
360 min-height: 100vh;
361 padding: 2rem;
362 color: #ffffff;
363 font-family: 'Courier New', monospace;
364}
365
366.header {
367 text-align: center;
368 margin-bottom: 2rem;
369 padding-bottom: 1rem;
370 border-bottom: 2px solid #00ff88;
371}
372
373.header h1 {
374 font-size: 2.5rem;
375 color: #00ff88;
376 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
377 margin: 0;
378}
379
380.subtitle {
381 color: #00b4d8;
382 font-size: 1.2rem;
383 margin-top: 0.5rem;
384}
385
386.connections-grid {
387 display: grid;
388 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
389 gap: 1.5rem;
390 margin-bottom: 2rem;
391}
392
393.connection-card {
394 background: rgba(255, 0, 85, 0.1);
395 border: 2px solid #ff0055;
396 border-radius: 10px;
397 padding: 1.5rem;
398 transition: all 0.3s ease;
399}
400
401.connection-card.connected {
402 background: rgba(0, 255, 136, 0.1);
403 border-color: #00ff88;
404}
405
406.connection-header {
407 display: flex;
408 justify-content: space-between;
409 align-items: center;
410 margin-bottom: 1.5rem;
411}
412
413.connection-header h3 {
414 color: #00ff88;
415 margin: 0;
416 font-size: 1.3rem;
417}
418
419.status-indicator {
420 padding: 0.5rem 1rem;
421 border-radius: 20px;
422 background: rgba(255, 0, 85, 0.3);
423 color: #ff0055;
424 font-weight: bold;
425 font-size: 0.9rem;
426}
427
428.status-indicator.active {
429 background: rgba(0, 255, 136, 0.3);
430 color: #00ff88;
431 animation: pulse 2s infinite;
432}
433
434.connection-stats {
435 display: grid;
436 grid-template-columns: 1fr 1fr;
437 gap: 1rem;
438 margin-bottom: 1rem;
439}
440
441.stat-item {
442 background: rgba(0, 0, 0, 0.3);
443 padding: 1rem;
444 border-radius: 5px;
445 text-align: center;
446}
447
448.stat-label {
449 display: block;
450 color: #00b4d8;
451 font-size: 0.8rem;
452 margin-bottom: 0.5rem;
453}
454
455.stat-value {
456 display: block;
457 color: #ffb400;
458 font-size: 1.5rem;
459 font-weight: bold;
460}
461
462.stat-value.good {
463 color: #00ff88;
464}
465
466.connection-times {
467 background: rgba(0, 0, 0, 0.3);
468 padding: 1rem;
469 border-radius: 5px;
470 margin-bottom: 1rem;
471 min-height: 60px;
472}
473
474.time-info {
475 color: #ffffff;
476 font-size: 0.9rem;
477 margin: 0.25rem 0;
478}
479
480.connection-controls {
481 display: flex;
482 gap: 0.5rem;
483 margin-bottom: 1rem;
484}
485
486.messages-log {
487 background: rgba(0, 0, 0, 0.5);
488 border-radius: 5px;
489 padding: 1rem;
490 max-height: 200px;
491 overflow-y: auto;
492}
493
494.log-message {
495 display: flex;
496 gap: 1rem;
497 padding: 0.5rem;
498 margin: 0.25rem 0;
499 border-left: 3px solid;
500 border-radius: 3px;
501 font-size: 0.85rem;
502}
503
504.msg-success {
505 border-color: #00ff88;
506 background: rgba(0, 255, 136, 0.1);
507}
508
509.msg-warning {
510 border-color: #ffb400;
511 background: rgba(255, 180, 0, 0.1);
512}
513
514.msg-error {
515 border-color: #ff0055;
516 background: rgba(255, 0, 85, 0.1);
517}
518
519.msg-info {
520 border-color: #00b4d8;
521 background: rgba(0, 180, 216, 0.1);
522}
523
524.msg-time {
525 color: #00b4d8;
526 min-width: 80px;
527 font-size: 0.75rem;
528}
529
530.msg-text {
531 color: #ffffff;
532 flex: 1;
533}
534
535.btn {
536 flex: 1;
537 padding: 0.75rem;
538 border: none;
539 border-radius: 5px;
540 font-weight: bold;
541 cursor: pointer;
542 font-family: 'Courier New', monospace;
543 transition: all 0.3s ease;
544 font-size: 0.9rem;
545}
546
547.btn:disabled {
548 opacity: 0.5;
549 cursor: not-allowed;
550}
551
552.btn-success {
553 background: #00ff88;
554 color: #0a0e27;
555}
556
557.btn-danger {
558 background: #ff0055;
559 color: #ffffff;
560}
561
562.btn-warning {
563 background: #ffb400;
564 color: #0a0e27;
565}
566
567.btn:not(:disabled):hover {
568 transform: scale(1.05);
569 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
570}
571
572.info-panel {
573 background: rgba(0, 180, 216, 0.05);
574 border: 2px solid #00b4d8;
575 border-radius: 10px;
576 padding: 2rem;
577}
578
579.info-panel h3 {
580 color: #00ff88;
581 margin-top: 0;
582 margin-bottom: 1.5rem;
583}
584
585.info-content ul {
586 list-style: none;
587 padding: 0;
588}
589
590.info-content li {
591 padding: 0.5rem 0;
592 color: #ffffff;
593 line-height: 1.6;
594}
595
596.code-example {
597 background: rgba(0, 0, 0, 0.5);
598 border-left: 4px solid #00ff88;
599 border-radius: 5px;
600 padding: 1rem;
601 margin-top: 1rem;
602}
603
604.code-example code {
605 color: #00ff88;
606 font-size: 0.9rem;
607 white-space: pre;
608 display: block;
609}
610
611@keyframes pulse {
612 0%, 100% { opacity: 1; }
613 50% { opacity: 0.6; }
614}
615</style>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que propose la bibliothèque VueUse ?
2. À quoi sert useLocalStorage de VueUse ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Crée un composant responsive avec les computed isMobile, isTablet, isDesktop.
- Tri vertical
Ordonne les étapes pour installer et utiliser VueUse :
- Clique dans l'ordre
Ordonne l'import d'un composable depuis VueUse :
- Éditeur de code
Utilise useDark et useToggle pour créer une bascule de mode sombre avec la classe CSS appropriée.
- Tri horizontal
Ordonne la déstructuration de useWindowSize :