hlarcher HF Staff commited on
Commit
b8952af
·
unverified ·
0 Parent(s):

Initial commit: Patrouille de France Airshow Tracker

Browse files

Single-page webapp to track Patrouille de France airshows with:
- Interactive Mapbox map with event markers
- Horizontal timeline with play/pause for auto-progression
- Animated flight paths between events
- Countdown to next upcoming meeting
- 2025 and 2026 airshow schedule
- French tricolor theme throughout

Tech stack: Vanilla JS, Vite, Mapbox GL JS v3

.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ VITE_MAPBOX_TOKEN=your_mapbox_token_here
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ !.env.example
5
+ .DS_Store
6
+ *.log
7
+ .claude/
8
+ .idea/
index.html ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Patrouille de France - Airshow Tracker</title>
7
+ <link href="https://api.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="/src/styles/main.css">
9
+ <link rel="stylesheet" href="/src/styles/timeline.css">
10
+ </head>
11
+ <body>
12
+ <div id="app">
13
+ <header class="header">
14
+ <h1>Patrouille de France</h1>
15
+ <span class="subtitle">Airshow Tracker 2025</span>
16
+ </header>
17
+
18
+ <main class="main">
19
+ <div id="map" class="map"></div>
20
+ <div id="countdown" class="countdown-container"></div>
21
+ <div id="event-card" class="event-card hidden"></div>
22
+ </main>
23
+
24
+ <footer class="footer">
25
+ <div id="timeline" class="timeline"></div>
26
+ </footer>
27
+ </div>
28
+
29
+ <script type="module" src="/src/main.js"></script>
30
+ </body>
31
+ </html>
package-lock.json ADDED
@@ -0,0 +1,1261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "paf-airshow-tracker",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "paf-airshow-tracker",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "mapbox-gl": "^3.9.4"
12
+ },
13
+ "devDependencies": {
14
+ "vite": "^6.0.0"
15
+ }
16
+ },
17
+ "node_modules/@esbuild/aix-ppc64": {
18
+ "version": "0.25.12",
19
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
20
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
21
+ "cpu": [
22
+ "ppc64"
23
+ ],
24
+ "dev": true,
25
+ "optional": true,
26
+ "os": [
27
+ "aix"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ }
32
+ },
33
+ "node_modules/@esbuild/android-arm": {
34
+ "version": "0.25.12",
35
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
36
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
37
+ "cpu": [
38
+ "arm"
39
+ ],
40
+ "dev": true,
41
+ "optional": true,
42
+ "os": [
43
+ "android"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18"
47
+ }
48
+ },
49
+ "node_modules/@esbuild/android-arm64": {
50
+ "version": "0.25.12",
51
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
52
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
53
+ "cpu": [
54
+ "arm64"
55
+ ],
56
+ "dev": true,
57
+ "optional": true,
58
+ "os": [
59
+ "android"
60
+ ],
61
+ "engines": {
62
+ "node": ">=18"
63
+ }
64
+ },
65
+ "node_modules/@esbuild/android-x64": {
66
+ "version": "0.25.12",
67
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
68
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
69
+ "cpu": [
70
+ "x64"
71
+ ],
72
+ "dev": true,
73
+ "optional": true,
74
+ "os": [
75
+ "android"
76
+ ],
77
+ "engines": {
78
+ "node": ">=18"
79
+ }
80
+ },
81
+ "node_modules/@esbuild/darwin-arm64": {
82
+ "version": "0.25.12",
83
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
84
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
85
+ "cpu": [
86
+ "arm64"
87
+ ],
88
+ "dev": true,
89
+ "optional": true,
90
+ "os": [
91
+ "darwin"
92
+ ],
93
+ "engines": {
94
+ "node": ">=18"
95
+ }
96
+ },
97
+ "node_modules/@esbuild/darwin-x64": {
98
+ "version": "0.25.12",
99
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
100
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
101
+ "cpu": [
102
+ "x64"
103
+ ],
104
+ "dev": true,
105
+ "optional": true,
106
+ "os": [
107
+ "darwin"
108
+ ],
109
+ "engines": {
110
+ "node": ">=18"
111
+ }
112
+ },
113
+ "node_modules/@esbuild/freebsd-arm64": {
114
+ "version": "0.25.12",
115
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
116
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
117
+ "cpu": [
118
+ "arm64"
119
+ ],
120
+ "dev": true,
121
+ "optional": true,
122
+ "os": [
123
+ "freebsd"
124
+ ],
125
+ "engines": {
126
+ "node": ">=18"
127
+ }
128
+ },
129
+ "node_modules/@esbuild/freebsd-x64": {
130
+ "version": "0.25.12",
131
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
132
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
133
+ "cpu": [
134
+ "x64"
135
+ ],
136
+ "dev": true,
137
+ "optional": true,
138
+ "os": [
139
+ "freebsd"
140
+ ],
141
+ "engines": {
142
+ "node": ">=18"
143
+ }
144
+ },
145
+ "node_modules/@esbuild/linux-arm": {
146
+ "version": "0.25.12",
147
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
148
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
149
+ "cpu": [
150
+ "arm"
151
+ ],
152
+ "dev": true,
153
+ "optional": true,
154
+ "os": [
155
+ "linux"
156
+ ],
157
+ "engines": {
158
+ "node": ">=18"
159
+ }
160
+ },
161
+ "node_modules/@esbuild/linux-arm64": {
162
+ "version": "0.25.12",
163
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
164
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
165
+ "cpu": [
166
+ "arm64"
167
+ ],
168
+ "dev": true,
169
+ "optional": true,
170
+ "os": [
171
+ "linux"
172
+ ],
173
+ "engines": {
174
+ "node": ">=18"
175
+ }
176
+ },
177
+ "node_modules/@esbuild/linux-ia32": {
178
+ "version": "0.25.12",
179
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
180
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
181
+ "cpu": [
182
+ "ia32"
183
+ ],
184
+ "dev": true,
185
+ "optional": true,
186
+ "os": [
187
+ "linux"
188
+ ],
189
+ "engines": {
190
+ "node": ">=18"
191
+ }
192
+ },
193
+ "node_modules/@esbuild/linux-loong64": {
194
+ "version": "0.25.12",
195
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
196
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
197
+ "cpu": [
198
+ "loong64"
199
+ ],
200
+ "dev": true,
201
+ "optional": true,
202
+ "os": [
203
+ "linux"
204
+ ],
205
+ "engines": {
206
+ "node": ">=18"
207
+ }
208
+ },
209
+ "node_modules/@esbuild/linux-mips64el": {
210
+ "version": "0.25.12",
211
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
212
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
213
+ "cpu": [
214
+ "mips64el"
215
+ ],
216
+ "dev": true,
217
+ "optional": true,
218
+ "os": [
219
+ "linux"
220
+ ],
221
+ "engines": {
222
+ "node": ">=18"
223
+ }
224
+ },
225
+ "node_modules/@esbuild/linux-ppc64": {
226
+ "version": "0.25.12",
227
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
228
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
229
+ "cpu": [
230
+ "ppc64"
231
+ ],
232
+ "dev": true,
233
+ "optional": true,
234
+ "os": [
235
+ "linux"
236
+ ],
237
+ "engines": {
238
+ "node": ">=18"
239
+ }
240
+ },
241
+ "node_modules/@esbuild/linux-riscv64": {
242
+ "version": "0.25.12",
243
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
244
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
245
+ "cpu": [
246
+ "riscv64"
247
+ ],
248
+ "dev": true,
249
+ "optional": true,
250
+ "os": [
251
+ "linux"
252
+ ],
253
+ "engines": {
254
+ "node": ">=18"
255
+ }
256
+ },
257
+ "node_modules/@esbuild/linux-s390x": {
258
+ "version": "0.25.12",
259
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
260
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
261
+ "cpu": [
262
+ "s390x"
263
+ ],
264
+ "dev": true,
265
+ "optional": true,
266
+ "os": [
267
+ "linux"
268
+ ],
269
+ "engines": {
270
+ "node": ">=18"
271
+ }
272
+ },
273
+ "node_modules/@esbuild/linux-x64": {
274
+ "version": "0.25.12",
275
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
276
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
277
+ "cpu": [
278
+ "x64"
279
+ ],
280
+ "dev": true,
281
+ "optional": true,
282
+ "os": [
283
+ "linux"
284
+ ],
285
+ "engines": {
286
+ "node": ">=18"
287
+ }
288
+ },
289
+ "node_modules/@esbuild/netbsd-arm64": {
290
+ "version": "0.25.12",
291
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
292
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
293
+ "cpu": [
294
+ "arm64"
295
+ ],
296
+ "dev": true,
297
+ "optional": true,
298
+ "os": [
299
+ "netbsd"
300
+ ],
301
+ "engines": {
302
+ "node": ">=18"
303
+ }
304
+ },
305
+ "node_modules/@esbuild/netbsd-x64": {
306
+ "version": "0.25.12",
307
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
308
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
309
+ "cpu": [
310
+ "x64"
311
+ ],
312
+ "dev": true,
313
+ "optional": true,
314
+ "os": [
315
+ "netbsd"
316
+ ],
317
+ "engines": {
318
+ "node": ">=18"
319
+ }
320
+ },
321
+ "node_modules/@esbuild/openbsd-arm64": {
322
+ "version": "0.25.12",
323
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
324
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
325
+ "cpu": [
326
+ "arm64"
327
+ ],
328
+ "dev": true,
329
+ "optional": true,
330
+ "os": [
331
+ "openbsd"
332
+ ],
333
+ "engines": {
334
+ "node": ">=18"
335
+ }
336
+ },
337
+ "node_modules/@esbuild/openbsd-x64": {
338
+ "version": "0.25.12",
339
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
340
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
341
+ "cpu": [
342
+ "x64"
343
+ ],
344
+ "dev": true,
345
+ "optional": true,
346
+ "os": [
347
+ "openbsd"
348
+ ],
349
+ "engines": {
350
+ "node": ">=18"
351
+ }
352
+ },
353
+ "node_modules/@esbuild/openharmony-arm64": {
354
+ "version": "0.25.12",
355
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
356
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
357
+ "cpu": [
358
+ "arm64"
359
+ ],
360
+ "dev": true,
361
+ "optional": true,
362
+ "os": [
363
+ "openharmony"
364
+ ],
365
+ "engines": {
366
+ "node": ">=18"
367
+ }
368
+ },
369
+ "node_modules/@esbuild/sunos-x64": {
370
+ "version": "0.25.12",
371
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
372
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
373
+ "cpu": [
374
+ "x64"
375
+ ],
376
+ "dev": true,
377
+ "optional": true,
378
+ "os": [
379
+ "sunos"
380
+ ],
381
+ "engines": {
382
+ "node": ">=18"
383
+ }
384
+ },
385
+ "node_modules/@esbuild/win32-arm64": {
386
+ "version": "0.25.12",
387
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
388
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
389
+ "cpu": [
390
+ "arm64"
391
+ ],
392
+ "dev": true,
393
+ "optional": true,
394
+ "os": [
395
+ "win32"
396
+ ],
397
+ "engines": {
398
+ "node": ">=18"
399
+ }
400
+ },
401
+ "node_modules/@esbuild/win32-ia32": {
402
+ "version": "0.25.12",
403
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
404
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
405
+ "cpu": [
406
+ "ia32"
407
+ ],
408
+ "dev": true,
409
+ "optional": true,
410
+ "os": [
411
+ "win32"
412
+ ],
413
+ "engines": {
414
+ "node": ">=18"
415
+ }
416
+ },
417
+ "node_modules/@esbuild/win32-x64": {
418
+ "version": "0.25.12",
419
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
420
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
421
+ "cpu": [
422
+ "x64"
423
+ ],
424
+ "dev": true,
425
+ "optional": true,
426
+ "os": [
427
+ "win32"
428
+ ],
429
+ "engines": {
430
+ "node": ">=18"
431
+ }
432
+ },
433
+ "node_modules/@mapbox/jsonlint-lines-primitives": {
434
+ "version": "2.0.2",
435
+ "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
436
+ "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
437
+ "engines": {
438
+ "node": ">= 0.6"
439
+ }
440
+ },
441
+ "node_modules/@mapbox/mapbox-gl-supported": {
442
+ "version": "3.0.0",
443
+ "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz",
444
+ "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg=="
445
+ },
446
+ "node_modules/@mapbox/point-geometry": {
447
+ "version": "1.1.0",
448
+ "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
449
+ "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="
450
+ },
451
+ "node_modules/@mapbox/tiny-sdf": {
452
+ "version": "2.0.7",
453
+ "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
454
+ "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug=="
455
+ },
456
+ "node_modules/@mapbox/unitbezier": {
457
+ "version": "0.0.1",
458
+ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
459
+ "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
460
+ },
461
+ "node_modules/@mapbox/vector-tile": {
462
+ "version": "2.0.4",
463
+ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
464
+ "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
465
+ "dependencies": {
466
+ "@mapbox/point-geometry": "~1.1.0",
467
+ "@types/geojson": "^7946.0.16",
468
+ "pbf": "^4.0.1"
469
+ }
470
+ },
471
+ "node_modules/@mapbox/whoots-js": {
472
+ "version": "3.1.0",
473
+ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
474
+ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
475
+ "engines": {
476
+ "node": ">=6.0.0"
477
+ }
478
+ },
479
+ "node_modules/@rollup/rollup-android-arm-eabi": {
480
+ "version": "4.57.1",
481
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
482
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
483
+ "cpu": [
484
+ "arm"
485
+ ],
486
+ "dev": true,
487
+ "optional": true,
488
+ "os": [
489
+ "android"
490
+ ]
491
+ },
492
+ "node_modules/@rollup/rollup-android-arm64": {
493
+ "version": "4.57.1",
494
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
495
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
496
+ "cpu": [
497
+ "arm64"
498
+ ],
499
+ "dev": true,
500
+ "optional": true,
501
+ "os": [
502
+ "android"
503
+ ]
504
+ },
505
+ "node_modules/@rollup/rollup-darwin-arm64": {
506
+ "version": "4.57.1",
507
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
508
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
509
+ "cpu": [
510
+ "arm64"
511
+ ],
512
+ "dev": true,
513
+ "optional": true,
514
+ "os": [
515
+ "darwin"
516
+ ]
517
+ },
518
+ "node_modules/@rollup/rollup-darwin-x64": {
519
+ "version": "4.57.1",
520
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
521
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
522
+ "cpu": [
523
+ "x64"
524
+ ],
525
+ "dev": true,
526
+ "optional": true,
527
+ "os": [
528
+ "darwin"
529
+ ]
530
+ },
531
+ "node_modules/@rollup/rollup-freebsd-arm64": {
532
+ "version": "4.57.1",
533
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
534
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
535
+ "cpu": [
536
+ "arm64"
537
+ ],
538
+ "dev": true,
539
+ "optional": true,
540
+ "os": [
541
+ "freebsd"
542
+ ]
543
+ },
544
+ "node_modules/@rollup/rollup-freebsd-x64": {
545
+ "version": "4.57.1",
546
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
547
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
548
+ "cpu": [
549
+ "x64"
550
+ ],
551
+ "dev": true,
552
+ "optional": true,
553
+ "os": [
554
+ "freebsd"
555
+ ]
556
+ },
557
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
558
+ "version": "4.57.1",
559
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
560
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
561
+ "cpu": [
562
+ "arm"
563
+ ],
564
+ "dev": true,
565
+ "optional": true,
566
+ "os": [
567
+ "linux"
568
+ ]
569
+ },
570
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
571
+ "version": "4.57.1",
572
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
573
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
574
+ "cpu": [
575
+ "arm"
576
+ ],
577
+ "dev": true,
578
+ "optional": true,
579
+ "os": [
580
+ "linux"
581
+ ]
582
+ },
583
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
584
+ "version": "4.57.1",
585
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
586
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
587
+ "cpu": [
588
+ "arm64"
589
+ ],
590
+ "dev": true,
591
+ "optional": true,
592
+ "os": [
593
+ "linux"
594
+ ]
595
+ },
596
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
597
+ "version": "4.57.1",
598
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
599
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
600
+ "cpu": [
601
+ "arm64"
602
+ ],
603
+ "dev": true,
604
+ "optional": true,
605
+ "os": [
606
+ "linux"
607
+ ]
608
+ },
609
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
610
+ "version": "4.57.1",
611
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
612
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
613
+ "cpu": [
614
+ "loong64"
615
+ ],
616
+ "dev": true,
617
+ "optional": true,
618
+ "os": [
619
+ "linux"
620
+ ]
621
+ },
622
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
623
+ "version": "4.57.1",
624
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
625
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
626
+ "cpu": [
627
+ "loong64"
628
+ ],
629
+ "dev": true,
630
+ "optional": true,
631
+ "os": [
632
+ "linux"
633
+ ]
634
+ },
635
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
636
+ "version": "4.57.1",
637
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
638
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
639
+ "cpu": [
640
+ "ppc64"
641
+ ],
642
+ "dev": true,
643
+ "optional": true,
644
+ "os": [
645
+ "linux"
646
+ ]
647
+ },
648
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
649
+ "version": "4.57.1",
650
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
651
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
652
+ "cpu": [
653
+ "ppc64"
654
+ ],
655
+ "dev": true,
656
+ "optional": true,
657
+ "os": [
658
+ "linux"
659
+ ]
660
+ },
661
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
662
+ "version": "4.57.1",
663
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
664
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
665
+ "cpu": [
666
+ "riscv64"
667
+ ],
668
+ "dev": true,
669
+ "optional": true,
670
+ "os": [
671
+ "linux"
672
+ ]
673
+ },
674
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
675
+ "version": "4.57.1",
676
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
677
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
678
+ "cpu": [
679
+ "riscv64"
680
+ ],
681
+ "dev": true,
682
+ "optional": true,
683
+ "os": [
684
+ "linux"
685
+ ]
686
+ },
687
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
688
+ "version": "4.57.1",
689
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
690
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
691
+ "cpu": [
692
+ "s390x"
693
+ ],
694
+ "dev": true,
695
+ "optional": true,
696
+ "os": [
697
+ "linux"
698
+ ]
699
+ },
700
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
701
+ "version": "4.57.1",
702
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
703
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
704
+ "cpu": [
705
+ "x64"
706
+ ],
707
+ "dev": true,
708
+ "optional": true,
709
+ "os": [
710
+ "linux"
711
+ ]
712
+ },
713
+ "node_modules/@rollup/rollup-linux-x64-musl": {
714
+ "version": "4.57.1",
715
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
716
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
717
+ "cpu": [
718
+ "x64"
719
+ ],
720
+ "dev": true,
721
+ "optional": true,
722
+ "os": [
723
+ "linux"
724
+ ]
725
+ },
726
+ "node_modules/@rollup/rollup-openbsd-x64": {
727
+ "version": "4.57.1",
728
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
729
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
730
+ "cpu": [
731
+ "x64"
732
+ ],
733
+ "dev": true,
734
+ "optional": true,
735
+ "os": [
736
+ "openbsd"
737
+ ]
738
+ },
739
+ "node_modules/@rollup/rollup-openharmony-arm64": {
740
+ "version": "4.57.1",
741
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
742
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
743
+ "cpu": [
744
+ "arm64"
745
+ ],
746
+ "dev": true,
747
+ "optional": true,
748
+ "os": [
749
+ "openharmony"
750
+ ]
751
+ },
752
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
753
+ "version": "4.57.1",
754
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
755
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
756
+ "cpu": [
757
+ "arm64"
758
+ ],
759
+ "dev": true,
760
+ "optional": true,
761
+ "os": [
762
+ "win32"
763
+ ]
764
+ },
765
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
766
+ "version": "4.57.1",
767
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
768
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
769
+ "cpu": [
770
+ "ia32"
771
+ ],
772
+ "dev": true,
773
+ "optional": true,
774
+ "os": [
775
+ "win32"
776
+ ]
777
+ },
778
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
779
+ "version": "4.57.1",
780
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
781
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
782
+ "cpu": [
783
+ "x64"
784
+ ],
785
+ "dev": true,
786
+ "optional": true,
787
+ "os": [
788
+ "win32"
789
+ ]
790
+ },
791
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
792
+ "version": "4.57.1",
793
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
794
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
795
+ "cpu": [
796
+ "x64"
797
+ ],
798
+ "dev": true,
799
+ "optional": true,
800
+ "os": [
801
+ "win32"
802
+ ]
803
+ },
804
+ "node_modules/@types/estree": {
805
+ "version": "1.0.8",
806
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
807
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
808
+ "dev": true
809
+ },
810
+ "node_modules/@types/geojson": {
811
+ "version": "7946.0.16",
812
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
813
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
814
+ },
815
+ "node_modules/@types/geojson-vt": {
816
+ "version": "3.2.5",
817
+ "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
818
+ "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
819
+ "dependencies": {
820
+ "@types/geojson": "*"
821
+ }
822
+ },
823
+ "node_modules/@types/mapbox__point-geometry": {
824
+ "version": "0.1.4",
825
+ "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
826
+ "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
827
+ },
828
+ "node_modules/@types/pbf": {
829
+ "version": "3.0.5",
830
+ "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
831
+ "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="
832
+ },
833
+ "node_modules/@types/supercluster": {
834
+ "version": "7.1.3",
835
+ "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
836
+ "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
837
+ "dependencies": {
838
+ "@types/geojson": "*"
839
+ }
840
+ },
841
+ "node_modules/cheap-ruler": {
842
+ "version": "4.0.0",
843
+ "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz",
844
+ "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw=="
845
+ },
846
+ "node_modules/csscolorparser": {
847
+ "version": "1.0.3",
848
+ "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
849
+ "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="
850
+ },
851
+ "node_modules/earcut": {
852
+ "version": "3.0.2",
853
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
854
+ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="
855
+ },
856
+ "node_modules/esbuild": {
857
+ "version": "0.25.12",
858
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
859
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
860
+ "dev": true,
861
+ "hasInstallScript": true,
862
+ "bin": {
863
+ "esbuild": "bin/esbuild"
864
+ },
865
+ "engines": {
866
+ "node": ">=18"
867
+ },
868
+ "optionalDependencies": {
869
+ "@esbuild/aix-ppc64": "0.25.12",
870
+ "@esbuild/android-arm": "0.25.12",
871
+ "@esbuild/android-arm64": "0.25.12",
872
+ "@esbuild/android-x64": "0.25.12",
873
+ "@esbuild/darwin-arm64": "0.25.12",
874
+ "@esbuild/darwin-x64": "0.25.12",
875
+ "@esbuild/freebsd-arm64": "0.25.12",
876
+ "@esbuild/freebsd-x64": "0.25.12",
877
+ "@esbuild/linux-arm": "0.25.12",
878
+ "@esbuild/linux-arm64": "0.25.12",
879
+ "@esbuild/linux-ia32": "0.25.12",
880
+ "@esbuild/linux-loong64": "0.25.12",
881
+ "@esbuild/linux-mips64el": "0.25.12",
882
+ "@esbuild/linux-ppc64": "0.25.12",
883
+ "@esbuild/linux-riscv64": "0.25.12",
884
+ "@esbuild/linux-s390x": "0.25.12",
885
+ "@esbuild/linux-x64": "0.25.12",
886
+ "@esbuild/netbsd-arm64": "0.25.12",
887
+ "@esbuild/netbsd-x64": "0.25.12",
888
+ "@esbuild/openbsd-arm64": "0.25.12",
889
+ "@esbuild/openbsd-x64": "0.25.12",
890
+ "@esbuild/openharmony-arm64": "0.25.12",
891
+ "@esbuild/sunos-x64": "0.25.12",
892
+ "@esbuild/win32-arm64": "0.25.12",
893
+ "@esbuild/win32-ia32": "0.25.12",
894
+ "@esbuild/win32-x64": "0.25.12"
895
+ }
896
+ },
897
+ "node_modules/fdir": {
898
+ "version": "6.5.0",
899
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
900
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
901
+ "dev": true,
902
+ "engines": {
903
+ "node": ">=12.0.0"
904
+ },
905
+ "peerDependencies": {
906
+ "picomatch": "^3 || ^4"
907
+ },
908
+ "peerDependenciesMeta": {
909
+ "picomatch": {
910
+ "optional": true
911
+ }
912
+ }
913
+ },
914
+ "node_modules/fsevents": {
915
+ "version": "2.3.3",
916
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
917
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
918
+ "dev": true,
919
+ "hasInstallScript": true,
920
+ "optional": true,
921
+ "os": [
922
+ "darwin"
923
+ ],
924
+ "engines": {
925
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
926
+ }
927
+ },
928
+ "node_modules/geojson-vt": {
929
+ "version": "4.0.2",
930
+ "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
931
+ "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="
932
+ },
933
+ "node_modules/gl-matrix": {
934
+ "version": "3.4.4",
935
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
936
+ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="
937
+ },
938
+ "node_modules/grid-index": {
939
+ "version": "1.1.0",
940
+ "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
941
+ "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA=="
942
+ },
943
+ "node_modules/kdbush": {
944
+ "version": "4.0.2",
945
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
946
+ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="
947
+ },
948
+ "node_modules/mapbox-gl": {
949
+ "version": "3.18.1",
950
+ "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.18.1.tgz",
951
+ "integrity": "sha512-Izc8dee2zkmb6Pn9hXFbVioPRLXJz1OFUcrvri69MhFACPU4bhLyVmhEsD9AyW1qOAP0Yvhzm60v63xdMIHPPw==",
952
+ "dependencies": {
953
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
954
+ "@mapbox/mapbox-gl-supported": "^3.0.0",
955
+ "@mapbox/point-geometry": "^1.1.0",
956
+ "@mapbox/tiny-sdf": "^2.0.6",
957
+ "@mapbox/unitbezier": "^0.0.1",
958
+ "@mapbox/vector-tile": "^2.0.4",
959
+ "@mapbox/whoots-js": "^3.1.0",
960
+ "@types/geojson": "^7946.0.16",
961
+ "@types/geojson-vt": "^3.2.5",
962
+ "@types/mapbox__point-geometry": "^0.1.4",
963
+ "@types/pbf": "^3.0.5",
964
+ "@types/supercluster": "^7.1.3",
965
+ "cheap-ruler": "^4.0.0",
966
+ "csscolorparser": "~1.0.3",
967
+ "earcut": "^3.0.1",
968
+ "geojson-vt": "^4.0.2",
969
+ "gl-matrix": "^3.4.4",
970
+ "grid-index": "^1.1.0",
971
+ "kdbush": "^4.0.2",
972
+ "martinez-polygon-clipping": "^0.8.1",
973
+ "murmurhash-js": "^1.0.0",
974
+ "pbf": "^4.0.1",
975
+ "potpack": "^2.0.0",
976
+ "quickselect": "^3.0.0",
977
+ "supercluster": "^8.0.1",
978
+ "tinyqueue": "^3.0.0"
979
+ }
980
+ },
981
+ "node_modules/martinez-polygon-clipping": {
982
+ "version": "0.8.1",
983
+ "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.8.1.tgz",
984
+ "integrity": "sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ==",
985
+ "dependencies": {
986
+ "robust-predicates": "^2.0.4",
987
+ "splaytree": "^0.1.4",
988
+ "tinyqueue": "3.0.0"
989
+ }
990
+ },
991
+ "node_modules/murmurhash-js": {
992
+ "version": "1.0.0",
993
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
994
+ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="
995
+ },
996
+ "node_modules/nanoid": {
997
+ "version": "3.3.11",
998
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
999
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1000
+ "dev": true,
1001
+ "funding": [
1002
+ {
1003
+ "type": "github",
1004
+ "url": "https://github.com/sponsors/ai"
1005
+ }
1006
+ ],
1007
+ "bin": {
1008
+ "nanoid": "bin/nanoid.cjs"
1009
+ },
1010
+ "engines": {
1011
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1012
+ }
1013
+ },
1014
+ "node_modules/pbf": {
1015
+ "version": "4.0.1",
1016
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
1017
+ "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
1018
+ "dependencies": {
1019
+ "resolve-protobuf-schema": "^2.1.0"
1020
+ },
1021
+ "bin": {
1022
+ "pbf": "bin/pbf"
1023
+ }
1024
+ },
1025
+ "node_modules/picocolors": {
1026
+ "version": "1.1.1",
1027
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1028
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1029
+ "dev": true
1030
+ },
1031
+ "node_modules/picomatch": {
1032
+ "version": "4.0.3",
1033
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1034
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1035
+ "dev": true,
1036
+ "engines": {
1037
+ "node": ">=12"
1038
+ },
1039
+ "funding": {
1040
+ "url": "https://github.com/sponsors/jonschlinkert"
1041
+ }
1042
+ },
1043
+ "node_modules/postcss": {
1044
+ "version": "8.5.6",
1045
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1046
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1047
+ "dev": true,
1048
+ "funding": [
1049
+ {
1050
+ "type": "opencollective",
1051
+ "url": "https://opencollective.com/postcss/"
1052
+ },
1053
+ {
1054
+ "type": "tidelift",
1055
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1056
+ },
1057
+ {
1058
+ "type": "github",
1059
+ "url": "https://github.com/sponsors/ai"
1060
+ }
1061
+ ],
1062
+ "dependencies": {
1063
+ "nanoid": "^3.3.11",
1064
+ "picocolors": "^1.1.1",
1065
+ "source-map-js": "^1.2.1"
1066
+ },
1067
+ "engines": {
1068
+ "node": "^10 || ^12 || >=14"
1069
+ }
1070
+ },
1071
+ "node_modules/potpack": {
1072
+ "version": "2.1.0",
1073
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
1074
+ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="
1075
+ },
1076
+ "node_modules/protocol-buffers-schema": {
1077
+ "version": "3.6.0",
1078
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
1079
+ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
1080
+ },
1081
+ "node_modules/quickselect": {
1082
+ "version": "3.0.0",
1083
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
1084
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
1085
+ },
1086
+ "node_modules/resolve-protobuf-schema": {
1087
+ "version": "2.1.0",
1088
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
1089
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
1090
+ "dependencies": {
1091
+ "protocol-buffers-schema": "^3.3.1"
1092
+ }
1093
+ },
1094
+ "node_modules/robust-predicates": {
1095
+ "version": "2.0.4",
1096
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz",
1097
+ "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="
1098
+ },
1099
+ "node_modules/rollup": {
1100
+ "version": "4.57.1",
1101
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
1102
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
1103
+ "dev": true,
1104
+ "dependencies": {
1105
+ "@types/estree": "1.0.8"
1106
+ },
1107
+ "bin": {
1108
+ "rollup": "dist/bin/rollup"
1109
+ },
1110
+ "engines": {
1111
+ "node": ">=18.0.0",
1112
+ "npm": ">=8.0.0"
1113
+ },
1114
+ "optionalDependencies": {
1115
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
1116
+ "@rollup/rollup-android-arm64": "4.57.1",
1117
+ "@rollup/rollup-darwin-arm64": "4.57.1",
1118
+ "@rollup/rollup-darwin-x64": "4.57.1",
1119
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
1120
+ "@rollup/rollup-freebsd-x64": "4.57.1",
1121
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
1122
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
1123
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
1124
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
1125
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
1126
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
1127
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
1128
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
1129
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
1130
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
1131
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
1132
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
1133
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
1134
+ "@rollup/rollup-openbsd-x64": "4.57.1",
1135
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
1136
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
1137
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
1138
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
1139
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
1140
+ "fsevents": "~2.3.2"
1141
+ }
1142
+ },
1143
+ "node_modules/source-map-js": {
1144
+ "version": "1.2.1",
1145
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1146
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1147
+ "dev": true,
1148
+ "engines": {
1149
+ "node": ">=0.10.0"
1150
+ }
1151
+ },
1152
+ "node_modules/splaytree": {
1153
+ "version": "0.1.4",
1154
+ "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-0.1.4.tgz",
1155
+ "integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ=="
1156
+ },
1157
+ "node_modules/supercluster": {
1158
+ "version": "8.0.1",
1159
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
1160
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
1161
+ "dependencies": {
1162
+ "kdbush": "^4.0.2"
1163
+ }
1164
+ },
1165
+ "node_modules/tinyglobby": {
1166
+ "version": "0.2.15",
1167
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
1168
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
1169
+ "dev": true,
1170
+ "dependencies": {
1171
+ "fdir": "^6.5.0",
1172
+ "picomatch": "^4.0.3"
1173
+ },
1174
+ "engines": {
1175
+ "node": ">=12.0.0"
1176
+ },
1177
+ "funding": {
1178
+ "url": "https://github.com/sponsors/SuperchupuDev"
1179
+ }
1180
+ },
1181
+ "node_modules/tinyqueue": {
1182
+ "version": "3.0.0",
1183
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
1184
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
1185
+ },
1186
+ "node_modules/vite": {
1187
+ "version": "6.4.1",
1188
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
1189
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
1190
+ "dev": true,
1191
+ "dependencies": {
1192
+ "esbuild": "^0.25.0",
1193
+ "fdir": "^6.4.4",
1194
+ "picomatch": "^4.0.2",
1195
+ "postcss": "^8.5.3",
1196
+ "rollup": "^4.34.9",
1197
+ "tinyglobby": "^0.2.13"
1198
+ },
1199
+ "bin": {
1200
+ "vite": "bin/vite.js"
1201
+ },
1202
+ "engines": {
1203
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1204
+ },
1205
+ "funding": {
1206
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1207
+ },
1208
+ "optionalDependencies": {
1209
+ "fsevents": "~2.3.3"
1210
+ },
1211
+ "peerDependencies": {
1212
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1213
+ "jiti": ">=1.21.0",
1214
+ "less": "*",
1215
+ "lightningcss": "^1.21.0",
1216
+ "sass": "*",
1217
+ "sass-embedded": "*",
1218
+ "stylus": "*",
1219
+ "sugarss": "*",
1220
+ "terser": "^5.16.0",
1221
+ "tsx": "^4.8.1",
1222
+ "yaml": "^2.4.2"
1223
+ },
1224
+ "peerDependenciesMeta": {
1225
+ "@types/node": {
1226
+ "optional": true
1227
+ },
1228
+ "jiti": {
1229
+ "optional": true
1230
+ },
1231
+ "less": {
1232
+ "optional": true
1233
+ },
1234
+ "lightningcss": {
1235
+ "optional": true
1236
+ },
1237
+ "sass": {
1238
+ "optional": true
1239
+ },
1240
+ "sass-embedded": {
1241
+ "optional": true
1242
+ },
1243
+ "stylus": {
1244
+ "optional": true
1245
+ },
1246
+ "sugarss": {
1247
+ "optional": true
1248
+ },
1249
+ "terser": {
1250
+ "optional": true
1251
+ },
1252
+ "tsx": {
1253
+ "optional": true
1254
+ },
1255
+ "yaml": {
1256
+ "optional": true
1257
+ }
1258
+ }
1259
+ }
1260
+ }
1261
+ }
package.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "paf-airshow-tracker",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "mapbox-gl": "^3.9.4"
12
+ },
13
+ "devDependencies": {
14
+ "vite": "^6.0.0"
15
+ }
16
+ }
src/components/Countdown.js ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MONTHS } from '../config.js';
2
+
3
+ export class Countdown {
4
+ constructor(container) {
5
+ this.container = container;
6
+ this.nextEvent = null;
7
+ this.intervalId = null;
8
+ }
9
+
10
+ setNextEvent(event) {
11
+ this.nextEvent = event;
12
+ this.render();
13
+ this.startTicking();
14
+ }
15
+
16
+ startTicking() {
17
+ if (this.intervalId) {
18
+ clearInterval(this.intervalId);
19
+ }
20
+ this.intervalId = setInterval(() => this.updateTime(), 1000);
21
+ }
22
+
23
+ getTimeUntil(date) {
24
+ const now = new Date();
25
+ const target = new Date(date);
26
+ target.setHours(14, 0, 0, 0); // Assume 14:00 start time
27
+
28
+ const diff = target - now;
29
+
30
+ if (diff <= 0) {
31
+ return { days: 0, hours: 0, minutes: 0, seconds: 0, passed: true };
32
+ }
33
+
34
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
35
+ const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
36
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
37
+ const seconds = Math.floor((diff % (1000 * 60)) / 1000);
38
+
39
+ return { days, hours, minutes, seconds, passed: false };
40
+ }
41
+
42
+ render() {
43
+ if (!this.nextEvent) {
44
+ this.container.innerHTML = '';
45
+ return;
46
+ }
47
+
48
+ const date = new Date(this.nextEvent.date);
49
+ const formattedDate = `${date.getDate()} ${MONTHS[date.getMonth()]}`;
50
+
51
+ this.container.innerHTML = `
52
+ <div class="countdown">
53
+ <div class="countdown-header">
54
+ <span class="countdown-label">Prochain meeting</span>
55
+ <span class="countdown-event">${this.nextEvent.name}</span>
56
+ <span class="countdown-location">${this.nextEvent.location.city} · ${formattedDate}</span>
57
+ </div>
58
+ <div class="countdown-timer">
59
+ <div class="countdown-unit">
60
+ <span class="countdown-value" data-unit="days">--</span>
61
+ <span class="countdown-unit-label">jours</span>
62
+ </div>
63
+ <div class="countdown-separator">:</div>
64
+ <div class="countdown-unit">
65
+ <span class="countdown-value" data-unit="hours">--</span>
66
+ <span class="countdown-unit-label">heures</span>
67
+ </div>
68
+ <div class="countdown-separator">:</div>
69
+ <div class="countdown-unit">
70
+ <span class="countdown-value" data-unit="minutes">--</span>
71
+ <span class="countdown-unit-label">min</span>
72
+ </div>
73
+ <div class="countdown-separator">:</div>
74
+ <div class="countdown-unit">
75
+ <span class="countdown-value" data-unit="seconds">--</span>
76
+ <span class="countdown-unit-label">sec</span>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ `;
81
+
82
+ this.updateTime();
83
+ }
84
+
85
+ updateTime() {
86
+ if (!this.nextEvent) return;
87
+
88
+ const time = this.getTimeUntil(this.nextEvent.date);
89
+
90
+ const daysEl = this.container.querySelector('[data-unit="days"]');
91
+ const hoursEl = this.container.querySelector('[data-unit="hours"]');
92
+ const minutesEl = this.container.querySelector('[data-unit="minutes"]');
93
+ const secondsEl = this.container.querySelector('[data-unit="seconds"]');
94
+
95
+ if (daysEl) daysEl.textContent = String(time.days).padStart(2, '0');
96
+ if (hoursEl) hoursEl.textContent = String(time.hours).padStart(2, '0');
97
+ if (minutesEl) minutesEl.textContent = String(time.minutes).padStart(2, '0');
98
+ if (secondsEl) secondsEl.textContent = String(time.seconds).padStart(2, '0');
99
+ }
100
+
101
+ destroy() {
102
+ if (this.intervalId) {
103
+ clearInterval(this.intervalId);
104
+ }
105
+ }
106
+ }
src/components/EventCard.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MONTHS } from '../config.js';
2
+
3
+ export class EventCard {
4
+ constructor(container) {
5
+ this.container = container;
6
+ this.currentEvent = null;
7
+ this.onClose = () => {};
8
+ }
9
+
10
+ show(event) {
11
+ this.currentEvent = event;
12
+ this.container.classList.remove('hidden');
13
+ this.render(event);
14
+ }
15
+
16
+ hide() {
17
+ this.container.classList.add('hidden');
18
+ this.currentEvent = null;
19
+ }
20
+
21
+ render(event) {
22
+ const date = new Date(event.date);
23
+ const formattedDate = `${date.getDate()} ${MONTHS[date.getMonth()]} ${date.getFullYear()}`;
24
+
25
+ this.container.innerHTML = `
26
+ <button class="close-btn" aria-label="Fermer">&times;</button>
27
+ <span class="event-type ${event.type}">${this.getTypeLabel(event.type)}</span>
28
+ <h2>${event.name}</h2>
29
+ <p class="event-location">${event.location.city}</p>
30
+ <p class="event-date">${formattedDate}</p>
31
+ `;
32
+
33
+ this.container.querySelector('.close-btn').addEventListener('click', () => {
34
+ this.hide();
35
+ this.onClose();
36
+ });
37
+ }
38
+
39
+ getTypeLabel(type) {
40
+ const labels = {
41
+ international: 'International',
42
+ national: 'National',
43
+ training: 'Entraînement'
44
+ };
45
+ return labels[type] || type;
46
+ }
47
+
48
+ setOnClose(callback) {
49
+ this.onClose = callback;
50
+ }
51
+ }
src/components/Map.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mapboxgl from 'mapbox-gl';
2
+ import { MAPBOX_TOKEN, MAP_CONFIG, HOME_BASE, COLORS } from '../config.js';
3
+ import { FlightPathLayer } from '../webgl/FlightPathLayer.js';
4
+
5
+ export class MapView {
6
+ constructor(container, options = {}) {
7
+ this.container = container;
8
+ this.markers = new window.Map();
9
+ this.homeMarker = null;
10
+ this.onEventSelect = options.onEventSelect || (() => {});
11
+
12
+ this.flightPathLayer = null;
13
+
14
+ this.init();
15
+ }
16
+
17
+ init() {
18
+ mapboxgl.accessToken = MAPBOX_TOKEN;
19
+
20
+ this.map = new mapboxgl.Map({
21
+ container: this.container,
22
+ ...MAP_CONFIG
23
+ });
24
+
25
+ this.map.on('load', () => {
26
+ this.addHomeBase();
27
+ this.flightPathLayer = new FlightPathLayer(this.map);
28
+ this.map.resize();
29
+ });
30
+
31
+ this.map.addControl(new mapboxgl.NavigationControl(), 'top-left');
32
+ }
33
+
34
+ addHomeBase() {
35
+ const el = document.createElement('div');
36
+ el.className = 'home-marker';
37
+ el.innerHTML = `
38
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
39
+ <circle cx="12" cy="12" r="10" fill="${COLORS.blue}" stroke="${COLORS.white}" stroke-width="2"/>
40
+ <path d="M12 6L16 14H8L12 6Z" fill="${COLORS.white}"/>
41
+ </svg>
42
+ `;
43
+
44
+ this.homeMarker = new mapboxgl.Marker({ element: el, anchor: 'center' })
45
+ .setLngLat(HOME_BASE.coordinates)
46
+ .addTo(this.map);
47
+ }
48
+
49
+ setEvents(events) {
50
+ this.events = events;
51
+
52
+ // Clear existing markers
53
+ this.markers.forEach(marker => marker.remove());
54
+ this.markers.clear();
55
+
56
+ events.forEach(event => {
57
+ const el = this.createMarkerElement(event);
58
+
59
+ const marker = new mapboxgl.Marker({ element: el, anchor: 'center' })
60
+ .setLngLat(event.location.coordinates)
61
+ .addTo(this.map);
62
+
63
+ el.addEventListener('click', () => {
64
+ this.onEventSelect(event);
65
+ });
66
+
67
+ this.markers.set(event.id, marker);
68
+ });
69
+ }
70
+
71
+ createMarkerElement(event) {
72
+ const el = document.createElement('div');
73
+ el.className = `event-marker ${event.type}`;
74
+
75
+ const color = event.type === 'international' ? COLORS.blue : COLORS.red;
76
+
77
+ el.innerHTML = `
78
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
79
+ <circle cx="10" cy="10" r="8" fill="${color}" opacity="0.3">
80
+ <animate attributeName="r" values="6;10;6" dur="2s" repeatCount="indefinite"/>
81
+ <animate attributeName="opacity" values="0.5;0.1;0.5" dur="2s" repeatCount="indefinite"/>
82
+ </circle>
83
+ <circle cx="10" cy="10" r="5" fill="${color}"/>
84
+ </svg>
85
+ `;
86
+
87
+ return el;
88
+ }
89
+
90
+ flyTo(coordinates, zoom = 9) {
91
+ this.map.flyTo({
92
+ center: coordinates,
93
+ zoom,
94
+ duration: 1500,
95
+ essential: true
96
+ });
97
+ }
98
+
99
+ selectEvent(eventId) {
100
+ // Reset all markers
101
+ this.markers.forEach((marker, id) => {
102
+ marker.getElement().classList.remove('selected');
103
+ });
104
+
105
+ // Highlight selected marker
106
+ const marker = this.markers.get(eventId);
107
+ if (marker) {
108
+ marker.getElement().classList.add('selected');
109
+ }
110
+ }
111
+
112
+ // Show full static path between two points
113
+ showFlightPath(from, to) {
114
+ if (this.flightPathLayer) {
115
+ this.flightPathLayer.showPath(from, to);
116
+ }
117
+ }
118
+
119
+ // Animate path being traced, call onComplete when done
120
+ animateFlightPath(from, to, duration, onComplete) {
121
+ if (this.flightPathLayer) {
122
+ this.flightPathLayer.animatePath(from, to, duration, onComplete);
123
+ }
124
+ }
125
+
126
+ hideFlightPath() {
127
+ if (this.flightPathLayer) {
128
+ this.flightPathLayer.hide();
129
+ }
130
+ }
131
+
132
+ resetView() {
133
+ this.map.flyTo({
134
+ center: MAP_CONFIG.center,
135
+ zoom: MAP_CONFIG.zoom,
136
+ duration: 1000
137
+ });
138
+
139
+ this.hideFlightPath();
140
+ }
141
+
142
+ getMap() {
143
+ return this.map;
144
+ }
145
+ }
src/components/Timeline.js ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MONTHS } from '../config.js';
2
+
3
+ export class Timeline {
4
+ constructor(container) {
5
+ this.container = container;
6
+ this.events = [];
7
+ this.sortedEvents = [];
8
+ this.selectedIndex = -1;
9
+ this.isPlaying = false;
10
+ this.onEventSelect = () => {};
11
+ this.onPlayStateChange = () => {};
12
+ }
13
+
14
+ setEvents(events) {
15
+ this.events = events;
16
+ // Sort events by date for playback
17
+ this.sortedEvents = [...events].sort((a, b) => new Date(a.date) - new Date(b.date));
18
+ this.render();
19
+ }
20
+
21
+ render() {
22
+ const grouped = this.groupByMonth(this.events);
23
+ const sortedKeys = Object.keys(grouped).sort();
24
+
25
+ let currentYear = null;
26
+ let monthsHtml = '';
27
+
28
+ for (const monthKey of sortedKeys) {
29
+ const [year] = monthKey.split('-');
30
+
31
+ // Add year separator when year changes
32
+ if (year !== currentYear) {
33
+ monthsHtml += `<div class="timeline-year">${year}</div>`;
34
+ currentYear = year;
35
+ }
36
+
37
+ monthsHtml += this.renderMonth(monthKey, grouped[monthKey]);
38
+ }
39
+
40
+ this.container.innerHTML = `
41
+ <button class="timeline-play-btn" aria-label="Play">
42
+ <svg class="play-icon" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
43
+ <path d="M6 4l10 6-10 6V4z"/>
44
+ </svg>
45
+ <svg class="pause-icon" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
46
+ <rect x="5" y="4" width="3" height="12"/>
47
+ <rect x="12" y="4" width="3" height="12"/>
48
+ </svg>
49
+ </button>
50
+ <div class="timeline-months">
51
+ ${monthsHtml}
52
+ </div>
53
+ `;
54
+
55
+ this.attachEventListeners();
56
+ }
57
+
58
+ groupByMonth(events) {
59
+ return events.reduce((acc, event) => {
60
+ const date = new Date(event.date);
61
+ const key = `${date.getFullYear()}-${String(date.getMonth()).padStart(2, '0')}`;
62
+
63
+ if (!acc[key]) {
64
+ acc[key] = [];
65
+ }
66
+ acc[key].push(event);
67
+ return acc;
68
+ }, {});
69
+ }
70
+
71
+ renderMonth(monthKey, events) {
72
+ const [year, month] = monthKey.split('-');
73
+ const monthName = MONTHS[parseInt(month)];
74
+
75
+ const eventsHtml = events
76
+ .sort((a, b) => new Date(a.date) - new Date(b.date))
77
+ .map(event => this.renderEvent(event))
78
+ .join('');
79
+
80
+ return `
81
+ <div class="timeline-month" data-month="${monthKey}">
82
+ <span class="timeline-month-label">${monthName}</span>
83
+ <div class="timeline-events">
84
+ ${eventsHtml}
85
+ </div>
86
+ </div>
87
+ `;
88
+ }
89
+
90
+ renderEvent(event) {
91
+ const date = new Date(event.date);
92
+ const day = date.getDate();
93
+ const eventIndex = this.sortedEvents.findIndex(e => e.id === event.id);
94
+ const isSelected = eventIndex === this.selectedIndex;
95
+
96
+ return `
97
+ <button
98
+ class="timeline-event ${event.type} ${isSelected ? 'selected' : ''}"
99
+ data-event-id="${event.id}"
100
+ data-event-index="${eventIndex}"
101
+ aria-label="${event.name} - ${day} ${MONTHS[date.getMonth()]}"
102
+ >
103
+ <span class="timeline-event-tooltip">
104
+ <strong>${event.name}</strong><br>
105
+ ${day} ${MONTHS[date.getMonth()]}
106
+ </span>
107
+ </button>
108
+ `;
109
+ }
110
+
111
+ attachEventListeners() {
112
+ // Play button
113
+ const playBtn = this.container.querySelector('.timeline-play-btn');
114
+ if (playBtn) {
115
+ playBtn.addEventListener('click', () => this.togglePlay());
116
+ }
117
+
118
+ // Event clicks
119
+ this.container.querySelectorAll('.timeline-event').forEach(el => {
120
+ el.addEventListener('click', () => {
121
+ const eventIndex = parseInt(el.dataset.eventIndex, 10);
122
+ this.selectEventByIndex(eventIndex);
123
+ });
124
+ });
125
+ }
126
+
127
+ togglePlay() {
128
+ this.isPlaying = !this.isPlaying;
129
+ this.updatePlayButton();
130
+ this.onPlayStateChange(this.isPlaying);
131
+ }
132
+
133
+ play() {
134
+ this.isPlaying = true;
135
+ this.updatePlayButton();
136
+ }
137
+
138
+ pause() {
139
+ this.isPlaying = false;
140
+ this.updatePlayButton();
141
+ }
142
+
143
+ updatePlayButton() {
144
+ const playBtn = this.container.querySelector('.timeline-play-btn');
145
+ if (playBtn) {
146
+ playBtn.classList.toggle('playing', this.isPlaying);
147
+ playBtn.setAttribute('aria-label', this.isPlaying ? 'Pause' : 'Play');
148
+ }
149
+ }
150
+
151
+ selectEventByIndex(index) {
152
+ if (index < 0 || index >= this.sortedEvents.length) return;
153
+
154
+ this.selectedIndex = index;
155
+ const event = this.sortedEvents[index];
156
+
157
+ // Update visual selection
158
+ this.container.querySelectorAll('.timeline-event').forEach(el => {
159
+ const elIndex = parseInt(el.dataset.eventIndex, 10);
160
+ el.classList.toggle('selected', elIndex === index);
161
+ });
162
+
163
+ // Scroll event into view
164
+ const eventEl = this.container.querySelector(`[data-event-index="${index}"]`);
165
+ if (eventEl) {
166
+ eventEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
167
+ }
168
+
169
+ this.onEventSelect(event, index);
170
+ }
171
+
172
+ selectEvent(eventId) {
173
+ const index = this.sortedEvents.findIndex(e => e.id === eventId);
174
+ if (index !== -1) {
175
+ this.selectEventByIndex(index);
176
+ }
177
+ }
178
+
179
+ // Advance to next event, returns false if at end
180
+ nextEvent() {
181
+ if (this.selectedIndex < this.sortedEvents.length - 1) {
182
+ this.selectEventByIndex(this.selectedIndex + 1);
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+
188
+ getCurrentEvent() {
189
+ if (this.selectedIndex >= 0 && this.selectedIndex < this.sortedEvents.length) {
190
+ return this.sortedEvents[this.selectedIndex];
191
+ }
192
+ return null;
193
+ }
194
+
195
+ getNextEvent() {
196
+ if (this.selectedIndex >= 0 && this.selectedIndex < this.sortedEvents.length - 1) {
197
+ return this.sortedEvents[this.selectedIndex + 1];
198
+ }
199
+ return null;
200
+ }
201
+
202
+ clearSelection() {
203
+ this.selectedIndex = -1;
204
+ this.container.querySelectorAll('.timeline-event').forEach(el => {
205
+ el.classList.remove('selected');
206
+ });
207
+ }
208
+
209
+ setOnEventSelect(callback) {
210
+ this.onEventSelect = callback;
211
+ }
212
+
213
+ setOnPlayStateChange(callback) {
214
+ this.onPlayStateChange = callback;
215
+ }
216
+ }
src/config.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_TOKEN;
2
+
3
+ export const MAP_CONFIG = {
4
+ style: 'mapbox://styles/mapbox/dark-v11',
5
+ center: [2.5, 46.5], // Center of France
6
+ zoom: 5,
7
+ minZoom: 4,
8
+ maxZoom: 12,
9
+ pitch: 0,
10
+ bearing: 0
11
+ };
12
+
13
+ export const HOME_BASE = {
14
+ name: 'Base aérienne 701 Salon-de-Provence',
15
+ coordinates: [5.1095, 43.6045]
16
+ };
17
+
18
+ export const COLORS = {
19
+ blue: '#0055A4',
20
+ white: '#FFFFFF',
21
+ red: '#EF4135'
22
+ };
23
+
24
+ export const MONTHS = [
25
+ 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
26
+ 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
27
+ ];
src/data/airshows.json ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "events": [
3
+ {
4
+ "id": "paris-airshow-2025",
5
+ "name": "Salon du Bourget",
6
+ "date": "2025-06-16",
7
+ "location": {
8
+ "city": "Le Bourget",
9
+ "coordinates": [2.4397, 48.9694]
10
+ },
11
+ "type": "international"
12
+ },
13
+ {
14
+ "id": "14-juillet-paris",
15
+ "name": "Défilé du 14 Juillet",
16
+ "date": "2025-07-14",
17
+ "location": {
18
+ "city": "Paris",
19
+ "coordinates": [2.3522, 48.8566]
20
+ },
21
+ "type": "national"
22
+ },
23
+ {
24
+ "id": "meeting-air-legend",
25
+ "name": "Meeting Air Legend",
26
+ "date": "2025-09-06",
27
+ "location": {
28
+ "city": "Melun-Villaroche",
29
+ "coordinates": [2.6717, 48.6047]
30
+ },
31
+ "type": "national"
32
+ },
33
+ {
34
+ "id": "free-flight-world-masters",
35
+ "name": "Free Flight World Masters",
36
+ "date": "2025-06-28",
37
+ "location": {
38
+ "city": "Béziers",
39
+ "coordinates": [3.2158, 43.3442]
40
+ },
41
+ "type": "national"
42
+ },
43
+ {
44
+ "id": "cap-dagde-airshow",
45
+ "name": "Cap d'Agde Airshow",
46
+ "date": "2025-07-05",
47
+ "location": {
48
+ "city": "Cap d'Agde",
49
+ "coordinates": [3.5128, 43.2802]
50
+ },
51
+ "type": "national"
52
+ },
53
+ {
54
+ "id": "rochefort-airshow",
55
+ "name": "Meeting Aérien de Rochefort",
56
+ "date": "2025-05-18",
57
+ "location": {
58
+ "city": "Rochefort",
59
+ "coordinates": [-0.9576, 45.9416]
60
+ },
61
+ "type": "national"
62
+ },
63
+ {
64
+ "id": "salon-provence-bapteme",
65
+ "name": "Baptême de la Promotion",
66
+ "date": "2025-05-23",
67
+ "location": {
68
+ "city": "Salon-de-Provence",
69
+ "coordinates": [5.1095, 43.6045]
70
+ },
71
+ "type": "national"
72
+ },
73
+ {
74
+ "id": "cannes-yachting",
75
+ "name": "Festival de Cannes",
76
+ "date": "2025-05-21",
77
+ "location": {
78
+ "city": "Cannes",
79
+ "coordinates": [7.0128, 43.5528]
80
+ },
81
+ "type": "national"
82
+ },
83
+ {
84
+ "id": "mont-de-marsan",
85
+ "name": "Meeting de Mont-de-Marsan",
86
+ "date": "2025-05-25",
87
+ "location": {
88
+ "city": "Mont-de-Marsan",
89
+ "coordinates": [-0.4942, 43.9117]
90
+ },
91
+ "type": "national"
92
+ },
93
+ {
94
+ "id": "lorient-airshow",
95
+ "name": "Meeting Aérien de Lorient",
96
+ "date": "2025-08-15",
97
+ "location": {
98
+ "city": "Lorient",
99
+ "coordinates": [-3.3800, 47.7500]
100
+ },
101
+ "type": "national"
102
+ },
103
+ {
104
+ "id": "la-ferte-alais",
105
+ "name": "Meeting de La Ferté-Alais",
106
+ "date": "2025-06-07",
107
+ "location": {
108
+ "city": "Cerny - La Ferté-Alais",
109
+ "coordinates": [2.3428, 48.4939]
110
+ },
111
+ "type": "national"
112
+ },
113
+ {
114
+ "id": "cazaux-airshow",
115
+ "name": "Meeting de Cazaux",
116
+ "date": "2025-06-14",
117
+ "location": {
118
+ "city": "Cazaux",
119
+ "coordinates": [-1.1250, 44.5333]
120
+ },
121
+ "type": "national"
122
+ },
123
+ {
124
+ "id": "dijon-airshow",
125
+ "name": "Meeting Aérien de Dijon",
126
+ "date": "2025-07-06",
127
+ "location": {
128
+ "city": "Dijon",
129
+ "coordinates": [5.0415, 47.3220]
130
+ },
131
+ "type": "national"
132
+ },
133
+ {
134
+ "id": "saint-dizier",
135
+ "name": "Meeting de Saint-Dizier",
136
+ "date": "2025-07-19",
137
+ "location": {
138
+ "city": "Saint-Dizier",
139
+ "coordinates": [4.9500, 48.6333]
140
+ },
141
+ "type": "national"
142
+ },
143
+ {
144
+ "id": "evreux-airshow",
145
+ "name": "Meeting d'Évreux",
146
+ "date": "2025-09-14",
147
+ "location": {
148
+ "city": "Évreux",
149
+ "coordinates": [1.2167, 49.0167]
150
+ },
151
+ "type": "national"
152
+ },
153
+ {
154
+ "id": "salon-provence-bapteme-2026",
155
+ "name": "Baptême de la Promotion",
156
+ "date": "2026-05-22",
157
+ "location": {
158
+ "city": "Salon-de-Provence",
159
+ "coordinates": [5.1095, 43.6045]
160
+ },
161
+ "type": "national"
162
+ },
163
+ {
164
+ "id": "cannes-2026",
165
+ "name": "Festival de Cannes",
166
+ "date": "2026-05-20",
167
+ "location": {
168
+ "city": "Cannes",
169
+ "coordinates": [7.0128, 43.5528]
170
+ },
171
+ "type": "national"
172
+ },
173
+ {
174
+ "id": "rochefort-2026",
175
+ "name": "Meeting Aérien de Rochefort",
176
+ "date": "2026-05-17",
177
+ "location": {
178
+ "city": "Rochefort",
179
+ "coordinates": [-0.9576, 45.9416]
180
+ },
181
+ "type": "national"
182
+ },
183
+ {
184
+ "id": "la-ferte-alais-2026",
185
+ "name": "Meeting de La Ferté-Alais",
186
+ "date": "2026-06-06",
187
+ "location": {
188
+ "city": "Cerny - La Ferté-Alais",
189
+ "coordinates": [2.3428, 48.4939]
190
+ },
191
+ "type": "national"
192
+ },
193
+ {
194
+ "id": "cazaux-2026",
195
+ "name": "Meeting de Cazaux",
196
+ "date": "2026-06-13",
197
+ "location": {
198
+ "city": "Cazaux",
199
+ "coordinates": [-1.1250, 44.5333]
200
+ },
201
+ "type": "national"
202
+ },
203
+ {
204
+ "id": "free-flight-2026",
205
+ "name": "Free Flight World Masters",
206
+ "date": "2026-06-27",
207
+ "location": {
208
+ "city": "Béziers",
209
+ "coordinates": [3.2158, 43.3442]
210
+ },
211
+ "type": "national"
212
+ },
213
+ {
214
+ "id": "cap-dagde-2026",
215
+ "name": "Cap d'Agde Airshow",
216
+ "date": "2026-07-04",
217
+ "location": {
218
+ "city": "Cap d'Agde",
219
+ "coordinates": [3.5128, 43.2802]
220
+ },
221
+ "type": "national"
222
+ },
223
+ {
224
+ "id": "14-juillet-2026",
225
+ "name": "Défilé du 14 Juillet",
226
+ "date": "2026-07-14",
227
+ "location": {
228
+ "city": "Paris",
229
+ "coordinates": [2.3522, 48.8566]
230
+ },
231
+ "type": "national"
232
+ },
233
+ {
234
+ "id": "dijon-2026",
235
+ "name": "Meeting Aérien de Dijon",
236
+ "date": "2026-07-05",
237
+ "location": {
238
+ "city": "Dijon",
239
+ "coordinates": [5.0415, 47.3220]
240
+ },
241
+ "type": "national"
242
+ },
243
+ {
244
+ "id": "saint-dizier-2026",
245
+ "name": "Meeting de Saint-Dizier",
246
+ "date": "2026-07-18",
247
+ "location": {
248
+ "city": "Saint-Dizier",
249
+ "coordinates": [4.9500, 48.6333]
250
+ },
251
+ "type": "national"
252
+ },
253
+ {
254
+ "id": "lorient-2026",
255
+ "name": "Meeting Aérien de Lorient",
256
+ "date": "2026-08-15",
257
+ "location": {
258
+ "city": "Lorient",
259
+ "coordinates": [-3.3800, 47.7500]
260
+ },
261
+ "type": "national"
262
+ },
263
+ {
264
+ "id": "air-legend-2026",
265
+ "name": "Meeting Air Legend",
266
+ "date": "2026-09-05",
267
+ "location": {
268
+ "city": "Melun-Villaroche",
269
+ "coordinates": [2.6717, 48.6047]
270
+ },
271
+ "type": "national"
272
+ },
273
+ {
274
+ "id": "evreux-2026",
275
+ "name": "Meeting d'Évreux",
276
+ "date": "2026-09-13",
277
+ "location": {
278
+ "city": "Évreux",
279
+ "coordinates": [1.2167, 49.0167]
280
+ },
281
+ "type": "national"
282
+ },
283
+ {
284
+ "id": "axalp-2026",
285
+ "name": "Axalp Fliegerschiessen",
286
+ "date": "2026-10-14",
287
+ "location": {
288
+ "city": "Axalp",
289
+ "coordinates": [8.0456, 46.7234]
290
+ },
291
+ "type": "international"
292
+ }
293
+ ],
294
+ "homeBase": {
295
+ "name": "Base aérienne 701 Salon-de-Provence",
296
+ "coordinates": [5.1095, 43.6045]
297
+ }
298
+ }
src/main.js ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MapView } from './components/Map.js';
2
+ import { Timeline } from './components/Timeline.js';
3
+ import { EventCard } from './components/EventCard.js';
4
+ import { Countdown } from './components/Countdown.js';
5
+ import { HOME_BASE } from './config.js';
6
+ import airshowData from './data/airshows.json';
7
+
8
+ const FLIGHT_ANIMATION_DURATION = 2000; // ms
9
+
10
+ class App {
11
+ constructor() {
12
+ this.map = null;
13
+ this.timeline = null;
14
+ this.eventCard = null;
15
+ this.countdown = null;
16
+ this.isPlaying = false;
17
+
18
+ this.init();
19
+ }
20
+
21
+ init() {
22
+ // Initialize event card
23
+ this.eventCard = new EventCard(document.getElementById('event-card'));
24
+ this.eventCard.setOnClose(() => this.handleCardClose());
25
+
26
+ // Initialize countdown
27
+ this.countdown = new Countdown(document.getElementById('countdown'));
28
+ this.initCountdown();
29
+
30
+ // Initialize map
31
+ this.map = new MapView('map', {
32
+ onEventSelect: (event) => this.handleMapEventClick(event)
33
+ });
34
+
35
+ // Initialize timeline
36
+ this.timeline = new Timeline(document.getElementById('timeline'));
37
+ this.timeline.setOnEventSelect((event, index) => this.handleTimelineSelect(event, index));
38
+ this.timeline.setOnPlayStateChange((playing) => this.handlePlayStateChange(playing));
39
+ this.timeline.setEvents(airshowData.events);
40
+
41
+ // Set events on map after it loads
42
+ this.map.getMap().on('load', () => {
43
+ this.map.setEvents(airshowData.events);
44
+ });
45
+
46
+ // Keyboard navigation
47
+ document.addEventListener('keydown', (e) => this.handleKeydown(e));
48
+ }
49
+
50
+ initCountdown() {
51
+ // Find next upcoming event
52
+ const now = new Date();
53
+ const sortedEvents = [...airshowData.events].sort((a, b) => new Date(a.date) - new Date(b.date));
54
+ const nextEvent = sortedEvents.find(e => new Date(e.date) >= now);
55
+
56
+ if (nextEvent) {
57
+ this.countdown.setNextEvent(nextEvent);
58
+ }
59
+ }
60
+
61
+ handleMapEventClick(event) {
62
+ // Clicking map marker pauses playback and selects event
63
+ if (this.isPlaying) {
64
+ this.timeline.pause();
65
+ this.isPlaying = false;
66
+ }
67
+ this.timeline.selectEvent(event.id);
68
+ }
69
+
70
+ handleTimelineSelect(event, index) {
71
+ // Update map marker selection
72
+ this.map.selectEvent(event.id);
73
+
74
+ // Show event card
75
+ this.eventCard.show(event);
76
+
77
+ // Get the "from" location (previous event or home base)
78
+ const prevEvent = index > 0 ? this.timeline.sortedEvents[index - 1] : null;
79
+ const fromCoords = prevEvent ? prevEvent.location.coordinates : HOME_BASE.coordinates;
80
+ const toCoords = event.location.coordinates;
81
+
82
+ if (this.isPlaying) {
83
+ // Animate the flight path being traced (no zoom/pan during playback)
84
+ this.map.animateFlightPath(fromCoords, toCoords, FLIGHT_ANIMATION_DURATION, () => {
85
+ // When animation completes, advance to next event
86
+ if (this.isPlaying) {
87
+ this.advanceToNextEvent();
88
+ }
89
+ });
90
+ } else {
91
+ // Show static full path to next event
92
+ this.map.flyTo(toCoords, 8);
93
+ const nextEvent = this.timeline.getNextEvent();
94
+ if (nextEvent) {
95
+ this.map.showFlightPath(toCoords, nextEvent.location.coordinates);
96
+ } else {
97
+ this.map.hideFlightPath();
98
+ }
99
+ }
100
+ }
101
+
102
+ handlePlayStateChange(playing) {
103
+ this.isPlaying = playing;
104
+
105
+ if (playing) {
106
+ // Start playback
107
+ if (this.timeline.selectedIndex < 0) {
108
+ // No event selected, start from first
109
+ this.timeline.selectEventByIndex(0);
110
+ } else {
111
+ // Re-trigger current selection to start animation
112
+ const currentEvent = this.timeline.getCurrentEvent();
113
+ if (currentEvent) {
114
+ this.handleTimelineSelect(currentEvent, this.timeline.selectedIndex);
115
+ }
116
+ }
117
+ } else {
118
+ // Paused - show static path to next event
119
+ const currentEvent = this.timeline.getCurrentEvent();
120
+ const nextEvent = this.timeline.getNextEvent();
121
+ if (currentEvent && nextEvent) {
122
+ this.map.showFlightPath(currentEvent.location.coordinates, nextEvent.location.coordinates);
123
+ }
124
+ }
125
+ }
126
+
127
+ advanceToNextEvent() {
128
+ const hasNext = this.timeline.nextEvent();
129
+ if (!hasNext) {
130
+ // Reached end, stop playing
131
+ this.timeline.pause();
132
+ this.isPlaying = false;
133
+ this.map.hideFlightPath();
134
+ }
135
+ }
136
+
137
+ handleCardClose() {
138
+ // Pause if playing
139
+ if (this.isPlaying) {
140
+ this.timeline.pause();
141
+ this.isPlaying = false;
142
+ }
143
+ this.timeline.clearSelection();
144
+ this.map.selectEvent(null);
145
+ this.map.resetView();
146
+ }
147
+
148
+ handleKeydown(e) {
149
+ if (e.key === 'Escape') {
150
+ this.handleCardClose();
151
+ } else if (e.key === ' ' || e.code === 'Space') {
152
+ e.preventDefault();
153
+ this.timeline.togglePlay();
154
+ } else if (e.key === 'ArrowRight' && !this.isPlaying) {
155
+ this.timeline.nextEvent();
156
+ }
157
+ }
158
+ }
159
+
160
+ // Start app
161
+ new App();
src/styles/main.css ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* French tricolor */
3
+ --color-blue: #0055A4;
4
+ --color-white: #FFFFFF;
5
+ --color-red: #EF4135;
6
+
7
+ /* UI colors */
8
+ --color-bg: #0a0a0f;
9
+ --color-bg-secondary: #12121a;
10
+ --color-text: #f0f0f5;
11
+ --color-text-muted: #8888a0;
12
+ --color-accent: #0055A4;
13
+
14
+ /* Spacing */
15
+ --header-height: 80px;
16
+ --timeline-height: 140px;
17
+
18
+ /* Typography */
19
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
20
+ }
21
+
22
+ * {
23
+ margin: 0;
24
+ padding: 0;
25
+ box-sizing: border-box;
26
+ }
27
+
28
+ html, body {
29
+ height: 100%;
30
+ overflow: hidden;
31
+ }
32
+
33
+ body {
34
+ font-family: var(--font-family);
35
+ background: var(--color-bg);
36
+ color: var(--color-text);
37
+ }
38
+
39
+ #app {
40
+ display: flex;
41
+ flex-direction: column;
42
+ height: 100vh;
43
+ }
44
+
45
+ /* Header */
46
+ .header {
47
+ height: var(--header-height);
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 16px;
51
+ padding: 0 24px;
52
+ background: var(--color-bg-secondary);
53
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
54
+ }
55
+
56
+ .header h1 {
57
+ font-size: 2rem;
58
+ font-weight: 700;
59
+ background: linear-gradient(90deg, var(--color-blue), var(--color-white), var(--color-red));
60
+ -webkit-background-clip: text;
61
+ -webkit-text-fill-color: transparent;
62
+ background-clip: text;
63
+ }
64
+
65
+ .header .subtitle {
66
+ font-size: 1rem;
67
+ color: var(--color-text-muted);
68
+ }
69
+
70
+ /* Main content */
71
+ .main {
72
+ flex: 1;
73
+ position: relative;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .map {
78
+ width: 100%;
79
+ height: 100%;
80
+ }
81
+
82
+ /* Countdown */
83
+ .countdown-container {
84
+ position: absolute;
85
+ bottom: 24px;
86
+ left: 24px;
87
+ z-index: 10;
88
+ }
89
+
90
+ .countdown {
91
+ background: linear-gradient(135deg, rgba(10, 10, 15, 0.95), rgba(18, 18, 26, 0.95));
92
+ border: 1px solid rgba(255, 255, 255, 0.1);
93
+ border-radius: 16px;
94
+ padding: 16px 24px;
95
+ backdrop-filter: blur(10px);
96
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
97
+ }
98
+
99
+ .countdown-header {
100
+ display: flex;
101
+ flex-direction: column;
102
+ gap: 2px;
103
+ margin-bottom: 12px;
104
+ padding-bottom: 12px;
105
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
106
+ }
107
+
108
+ .countdown-label {
109
+ font-size: 0.65rem;
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.15em;
112
+ color: var(--color-text-muted);
113
+ }
114
+
115
+ .countdown-event {
116
+ font-size: 1rem;
117
+ font-weight: 600;
118
+ background: linear-gradient(90deg, var(--color-blue), var(--color-white), var(--color-red));
119
+ -webkit-background-clip: text;
120
+ -webkit-text-fill-color: transparent;
121
+ background-clip: text;
122
+ }
123
+
124
+ .countdown-location {
125
+ font-size: 0.75rem;
126
+ color: var(--color-text-muted);
127
+ }
128
+
129
+ .countdown-timer {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 8px;
133
+ }
134
+
135
+ .countdown-unit {
136
+ display: flex;
137
+ flex-direction: column;
138
+ align-items: center;
139
+ min-width: 44px;
140
+ }
141
+
142
+ .countdown-value {
143
+ font-size: 1.75rem;
144
+ font-weight: 700;
145
+ font-variant-numeric: tabular-nums;
146
+ color: var(--color-text);
147
+ line-height: 1;
148
+ }
149
+
150
+ .countdown-unit:first-child .countdown-value {
151
+ color: var(--color-blue);
152
+ }
153
+
154
+ .countdown-unit:nth-child(3) .countdown-value {
155
+ color: var(--color-white);
156
+ }
157
+
158
+ .countdown-unit:nth-child(5) .countdown-value {
159
+ color: var(--color-white);
160
+ }
161
+
162
+ .countdown-unit:last-child .countdown-value {
163
+ color: var(--color-red);
164
+ }
165
+
166
+ .countdown-unit-label {
167
+ font-size: 0.6rem;
168
+ text-transform: uppercase;
169
+ letter-spacing: 0.1em;
170
+ color: var(--color-text-muted);
171
+ margin-top: 4px;
172
+ }
173
+
174
+ .countdown-separator {
175
+ font-size: 1.5rem;
176
+ font-weight: 300;
177
+ color: var(--color-text-muted);
178
+ margin-bottom: 16px;
179
+ }
180
+
181
+ /* Event Card */
182
+ .event-card {
183
+ position: absolute;
184
+ top: 20px;
185
+ right: 20px;
186
+ width: 300px;
187
+ background: var(--color-bg-secondary);
188
+ border: 1px solid rgba(255, 255, 255, 0.1);
189
+ border-radius: 12px;
190
+ padding: 20px;
191
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
192
+ z-index: 10;
193
+ transition: opacity 0.3s, transform 0.3s;
194
+ }
195
+
196
+ .event-card.hidden {
197
+ opacity: 0;
198
+ pointer-events: none;
199
+ transform: translateY(-10px);
200
+ }
201
+
202
+ .event-card .event-type {
203
+ display: inline-block;
204
+ font-size: 0.75rem;
205
+ font-weight: 600;
206
+ text-transform: uppercase;
207
+ letter-spacing: 0.05em;
208
+ padding: 4px 8px;
209
+ border-radius: 4px;
210
+ margin-bottom: 12px;
211
+ }
212
+
213
+ .event-card .event-type.international {
214
+ background: rgba(0, 85, 164, 0.3);
215
+ color: var(--color-blue);
216
+ }
217
+
218
+ .event-card .event-type.national {
219
+ background: rgba(239, 65, 53, 0.3);
220
+ color: var(--color-red);
221
+ }
222
+
223
+ .event-card .event-type.training {
224
+ background: rgba(255, 255, 255, 0.2);
225
+ color: var(--color-text-muted);
226
+ }
227
+
228
+ .event-card h2 {
229
+ font-size: 1.25rem;
230
+ margin-bottom: 8px;
231
+ }
232
+
233
+ .event-card .event-location {
234
+ color: var(--color-text-muted);
235
+ font-size: 0.875rem;
236
+ margin-bottom: 8px;
237
+ }
238
+
239
+ .event-card .event-date {
240
+ font-size: 0.875rem;
241
+ color: var(--color-text);
242
+ }
243
+
244
+ .event-card .close-btn {
245
+ position: absolute;
246
+ top: 12px;
247
+ right: 12px;
248
+ background: none;
249
+ border: none;
250
+ color: var(--color-text-muted);
251
+ cursor: pointer;
252
+ font-size: 1.25rem;
253
+ line-height: 1;
254
+ padding: 4px;
255
+ }
256
+
257
+ .event-card .close-btn:hover {
258
+ color: var(--color-text);
259
+ }
260
+
261
+ /* Footer */
262
+ .footer {
263
+ height: var(--timeline-height);
264
+ background: var(--color-bg-secondary);
265
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
266
+ }
267
+
268
+ /* Loading state */
269
+ .loading {
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ height: 100%;
274
+ color: var(--color-text-muted);
275
+ }
276
+
277
+ /* Mapbox overrides */
278
+ .mapboxgl-ctrl-logo,
279
+ .mapboxgl-ctrl-attrib {
280
+ opacity: 0.5;
281
+ }
282
+
283
+ /* Map markers */
284
+ .home-marker {
285
+ cursor: pointer;
286
+ }
287
+
288
+ .event-marker {
289
+ cursor: pointer;
290
+ }
291
+
292
+ .event-marker svg {
293
+ transition: transform 0.2s;
294
+ }
295
+
296
+ .event-marker:hover svg {
297
+ transform: scale(1.2);
298
+ }
299
+
300
+ .event-marker.selected svg {
301
+ transform: scale(1.4);
302
+ }
303
+
304
+ .event-marker.selected svg circle:last-child {
305
+ stroke: var(--color-white);
306
+ stroke-width: 2;
307
+ }
src/styles/timeline.css ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .timeline {
2
+ height: 100%;
3
+ display: flex;
4
+ align-items: center;
5
+ padding: 16px 24px;
6
+ gap: 16px;
7
+ overflow-x: auto;
8
+ scrollbar-width: thin;
9
+ scrollbar-color: var(--color-text-muted) transparent;
10
+ }
11
+
12
+ /* Play/Pause button */
13
+ .timeline-play-btn {
14
+ flex-shrink: 0;
15
+ align-self: center;
16
+ width: 48px;
17
+ height: 48px;
18
+ border-radius: 50%;
19
+ border: 2px solid var(--color-text-muted);
20
+ background: transparent;
21
+ color: var(--color-text);
22
+ cursor: pointer;
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: center;
26
+ transition: border-color 0.2s, background 0.2s, transform 0.2s;
27
+ }
28
+
29
+ .timeline-play-btn:hover {
30
+ border-color: var(--color-text);
31
+ background: rgba(255, 255, 255, 0.1);
32
+ }
33
+
34
+ .timeline-play-btn:active {
35
+ transform: scale(0.95);
36
+ }
37
+
38
+ .timeline-play-btn .pause-icon {
39
+ display: none;
40
+ }
41
+
42
+ .timeline-play-btn.playing .play-icon {
43
+ display: none;
44
+ }
45
+
46
+ .timeline-play-btn.playing .pause-icon {
47
+ display: block;
48
+ }
49
+
50
+ .timeline-play-btn.playing {
51
+ border-color: var(--color-blue);
52
+ background: rgba(0, 85, 164, 0.2);
53
+ }
54
+
55
+ .timeline::-webkit-scrollbar {
56
+ height: 6px;
57
+ }
58
+
59
+ .timeline::-webkit-scrollbar-track {
60
+ background: transparent;
61
+ }
62
+
63
+ .timeline::-webkit-scrollbar-thumb {
64
+ background: var(--color-text-muted);
65
+ border-radius: 3px;
66
+ }
67
+
68
+ .timeline-months {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 24px;
72
+ padding: 16px 24px 16px 0;
73
+ flex-shrink: 0;
74
+ }
75
+
76
+ .timeline-year {
77
+ font-size: 1.5rem;
78
+ font-weight: 700;
79
+ color: var(--color-text);
80
+ padding: 0 8px;
81
+ opacity: 0.3;
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .timeline-month {
86
+ display: flex;
87
+ flex-direction: column;
88
+ align-items: flex-start;
89
+ gap: 8px;
90
+ }
91
+
92
+ .timeline-month-label {
93
+ font-size: 0.7rem;
94
+ font-weight: 600;
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.1em;
97
+ color: var(--color-text-muted);
98
+ white-space: nowrap;
99
+ }
100
+
101
+ .timeline-events {
102
+ display: flex;
103
+ gap: 10px;
104
+ align-items: center;
105
+ }
106
+
107
+ .timeline-event {
108
+ position: relative;
109
+ width: 16px;
110
+ height: 16px;
111
+ border-radius: 50%;
112
+ cursor: pointer;
113
+ transition: transform 0.2s, box-shadow 0.2s;
114
+ border: 2px solid transparent;
115
+ }
116
+
117
+ .timeline-event::before {
118
+ content: '';
119
+ position: absolute;
120
+ inset: -4px;
121
+ border-radius: 50%;
122
+ background: radial-gradient(circle, currentColor 0%, transparent 70%);
123
+ opacity: 0;
124
+ transition: opacity 0.2s;
125
+ }
126
+
127
+ .timeline-event:hover {
128
+ transform: scale(1.3);
129
+ }
130
+
131
+ .timeline-event:hover::before {
132
+ opacity: 0.3;
133
+ }
134
+
135
+ .timeline-event.selected {
136
+ transform: scale(1.4);
137
+ border-color: var(--color-white);
138
+ }
139
+
140
+ .timeline-event.selected::before {
141
+ opacity: 0.5;
142
+ }
143
+
144
+ /* Event type colors */
145
+ .timeline-event.international {
146
+ background: var(--color-blue);
147
+ color: var(--color-blue);
148
+ }
149
+
150
+ .timeline-event.national {
151
+ background: var(--color-red);
152
+ color: var(--color-red);
153
+ }
154
+
155
+ .timeline-event.training {
156
+ background: var(--color-text-muted);
157
+ color: var(--color-text-muted);
158
+ }
159
+
160
+ /* Tooltip - appears below the dot */
161
+ .timeline-event-tooltip {
162
+ position: absolute;
163
+ top: calc(100% + 8px);
164
+ left: 50%;
165
+ transform: translateX(-50%);
166
+ background: var(--color-bg);
167
+ border: 1px solid rgba(255, 255, 255, 0.15);
168
+ border-radius: 4px;
169
+ padding: 4px 8px;
170
+ white-space: nowrap;
171
+ font-size: 0.65rem;
172
+ line-height: 1.4;
173
+ opacity: 0;
174
+ pointer-events: none;
175
+ transition: opacity 0.15s;
176
+ z-index: 100;
177
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
178
+ }
179
+
180
+ .timeline-event-tooltip strong {
181
+ font-weight: 600;
182
+ }
183
+
184
+ .timeline-event:hover .timeline-event-tooltip {
185
+ opacity: 1;
186
+ }
187
+
188
+ .timeline-event-tooltip::after {
189
+ content: '';
190
+ position: absolute;
191
+ bottom: 100%;
192
+ left: 50%;
193
+ transform: translateX(-50%);
194
+ border: 4px solid transparent;
195
+ border-bottom-color: var(--color-bg);
196
+ }
197
+
198
+ /* Mobile: vertical timeline */
199
+ @media (max-width: 768px) {
200
+ :root {
201
+ --timeline-height: 200px;
202
+ }
203
+
204
+ .timeline {
205
+ flex-direction: column;
206
+ overflow-y: auto;
207
+ overflow-x: hidden;
208
+ align-items: flex-start;
209
+ }
210
+
211
+ .timeline-months {
212
+ flex-direction: column;
213
+ gap: 16px;
214
+ }
215
+
216
+ .timeline-month {
217
+ flex-direction: row;
218
+ align-items: center;
219
+ gap: 16px;
220
+ min-width: auto;
221
+ }
222
+
223
+ .timeline-month-label {
224
+ min-width: 60px;
225
+ }
226
+ }
src/webgl/FlightPathLayer.js ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { COLORS } from '../config.js';
2
+
3
+ export class FlightPathLayer {
4
+ constructor(map) {
5
+ this.map = map;
6
+ this.id = 'flight-path';
7
+ this.sourceId = 'flight-path-source';
8
+ this.animationFrame = null;
9
+ this.fullPath = null;
10
+ this.layersAdded = false;
11
+ }
12
+
13
+ createArcPath(start, end, numPoints = 50) {
14
+ const points = [];
15
+ const [x1, y1] = start;
16
+ const [x2, y2] = end;
17
+
18
+ const dx = x2 - x1;
19
+ const dy = y2 - y1;
20
+ const distance = Math.sqrt(dx * dx + dy * dy);
21
+ const arcHeight = distance * 0.15;
22
+
23
+ for (let i = 0; i <= numPoints; i++) {
24
+ const t = i / numPoints;
25
+ const x = x1 + t * dx;
26
+ const y = y1 + t * dy;
27
+ const arcOffset = arcHeight * Math.sin(t * Math.PI);
28
+ points.push([x, y + arcOffset]);
29
+ }
30
+
31
+ return points;
32
+ }
33
+
34
+ ensureLayers() {
35
+ if (this.layersAdded) return;
36
+
37
+ // Add source with empty data
38
+ if (!this.map.getSource(this.sourceId)) {
39
+ this.map.addSource(this.sourceId, {
40
+ type: 'geojson',
41
+ data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [] } }
42
+ });
43
+ }
44
+
45
+ // Glow layer
46
+ if (!this.map.getLayer(`${this.id}-glow`)) {
47
+ this.map.addLayer({
48
+ id: `${this.id}-glow`,
49
+ type: 'line',
50
+ source: this.sourceId,
51
+ paint: {
52
+ 'line-color': COLORS.blue,
53
+ 'line-width': 8,
54
+ 'line-opacity': 0.3,
55
+ 'line-blur': 4
56
+ }
57
+ });
58
+ }
59
+
60
+ // Main white line
61
+ if (!this.map.getLayer(`${this.id}-main`)) {
62
+ this.map.addLayer({
63
+ id: `${this.id}-main`,
64
+ type: 'line',
65
+ source: this.sourceId,
66
+ paint: {
67
+ 'line-color': COLORS.white,
68
+ 'line-width': 2,
69
+ 'line-opacity': 0.9
70
+ }
71
+ });
72
+ }
73
+
74
+ // Blue accent
75
+ if (!this.map.getLayer(`${this.id}-blue`)) {
76
+ this.map.addLayer({
77
+ id: `${this.id}-blue`,
78
+ type: 'line',
79
+ source: this.sourceId,
80
+ paint: {
81
+ 'line-color': COLORS.blue,
82
+ 'line-width': 2,
83
+ 'line-opacity': 0.7,
84
+ 'line-offset': 3
85
+ }
86
+ });
87
+ }
88
+
89
+ // Red accent
90
+ if (!this.map.getLayer(`${this.id}-red`)) {
91
+ this.map.addLayer({
92
+ id: `${this.id}-red`,
93
+ type: 'line',
94
+ source: this.sourceId,
95
+ paint: {
96
+ 'line-color': COLORS.red,
97
+ 'line-width': 2,
98
+ 'line-opacity': 0.7,
99
+ 'line-offset': -3
100
+ }
101
+ });
102
+ }
103
+
104
+ this.layersAdded = true;
105
+ }
106
+
107
+ updatePath(coordinates) {
108
+ const source = this.map.getSource(this.sourceId);
109
+ if (source) {
110
+ source.setData({
111
+ type: 'Feature',
112
+ geometry: { type: 'LineString', coordinates }
113
+ });
114
+ }
115
+ }
116
+
117
+ // Show full static path
118
+ showPath(from, to) {
119
+ this.stopAnimation();
120
+ this.ensureLayers();
121
+
122
+ this.fullPath = this.createArcPath(from, to);
123
+ this.updatePath(this.fullPath);
124
+ }
125
+
126
+ // Animate path being traced progressively
127
+ animatePath(from, to, duration, onComplete) {
128
+ this.stopAnimation();
129
+ this.ensureLayers();
130
+
131
+ this.fullPath = this.createArcPath(from, to);
132
+ const totalPoints = this.fullPath.length;
133
+ const startTime = performance.now();
134
+
135
+ const animate = (currentTime) => {
136
+ const elapsed = currentTime - startTime;
137
+ const progress = Math.min(elapsed / duration, 1);
138
+
139
+ // Ease out cubic for smooth deceleration
140
+ const eased = 1 - Math.pow(1 - progress, 3);
141
+ const pointCount = Math.max(2, Math.floor(eased * totalPoints));
142
+
143
+ this.updatePath(this.fullPath.slice(0, pointCount));
144
+
145
+ if (progress < 1) {
146
+ this.animationFrame = requestAnimationFrame(animate);
147
+ } else {
148
+ this.animationFrame = null;
149
+ if (onComplete) onComplete();
150
+ }
151
+ };
152
+
153
+ this.animationFrame = requestAnimationFrame(animate);
154
+ }
155
+
156
+ stopAnimation() {
157
+ if (this.animationFrame) {
158
+ cancelAnimationFrame(this.animationFrame);
159
+ this.animationFrame = null;
160
+ }
161
+ }
162
+
163
+ hide() {
164
+ this.stopAnimation();
165
+ this.updatePath([]);
166
+ this.fullPath = null;
167
+ }
168
+
169
+ remove() {
170
+ this.hide();
171
+
172
+ ['glow', 'main', 'blue', 'red'].forEach(suffix => {
173
+ if (this.map.getLayer(`${this.id}-${suffix}`)) {
174
+ this.map.removeLayer(`${this.id}-${suffix}`);
175
+ }
176
+ });
177
+
178
+ if (this.map.getSource(this.sourceId)) {
179
+ this.map.removeSource(this.sourceId);
180
+ }
181
+
182
+ this.layersAdded = false;
183
+ }
184
+ }
vite.config.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ server: {
5
+ open: true
6
+ },
7
+ build: {
8
+ target: 'esnext'
9
+ }
10
+ });