@workadventure/map-starter-kit-core 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +165 -0
  2. package/dist/assets/js/index.js +164 -0
  3. package/dist/assets/views/index.html +71 -0
  4. package/dist/assets/views/step1-git.html +154 -0
  5. package/dist/assets/views/step2-hosting.html +153 -0
  6. package/dist/assets/views/step3-steps-selfhosted.html +502 -0
  7. package/dist/assets/views/step3-steps.html +549 -0
  8. package/dist/assets/views/step4-validated-selfhosted.html +188 -0
  9. package/dist/assets/views/step4-validated.html +80 -0
  10. package/dist/images/world-select.png +0 -0
  11. package/dist/server.js +4 -4
  12. package/dist/server.js.map +1 -1
  13. package/dist/styles/styles.css +1 -8
  14. package/package.json +3 -1
  15. package/public/assets/js/index.js +164 -0
  16. package/public/assets/views/index.html +71 -0
  17. package/public/assets/views/step1-git.html +154 -0
  18. package/public/assets/views/step2-hosting.html +153 -0
  19. package/public/assets/views/step3-steps-selfhosted.html +502 -0
  20. package/public/assets/views/step3-steps.html +549 -0
  21. package/public/assets/views/step4-validated-selfhosted.html +188 -0
  22. package/public/assets/views/step4-validated.html +80 -0
  23. package/public/images/badumtss.svg +12 -0
  24. package/public/images/brand-discord.svg +22 -0
  25. package/public/images/brand-github.svg +19 -0
  26. package/public/images/brand-linkedin.svg +23 -0
  27. package/public/images/brand-x.svg +20 -0
  28. package/public/images/brand-youtube.svg +20 -0
  29. package/public/images/favicon.svg +50 -0
  30. package/public/images/logo.svg +72 -0
  31. package/public/images/world-select.png +0 -0
  32. package/public/styles/styles.css +424 -0
  33. package/public/styles/styles.css.map +1 -0
  34. package/types/server.d.ts +5 -0
  35. package/dist/assets/index.js +0 -80
  36. /package/{dist/images/unknown-room-image copy.png → public/images/unknown-room-image.png} +0 -0
@@ -0,0 +1,153 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <meta name="robots" content="noindex">
9
+ <meta name="title" content="WorkAdventure Starter Kit">
10
+
11
+ <link href="public/styles/styles.css" rel="stylesheet">
12
+
13
+ <title>WorkAdventure build your map</title>
14
+ <link rel="icon" href="public/images/favicon.svg" type="image/svg+xml">
15
+ </head>
16
+
17
+ <body>
18
+ <div class="content">
19
+ <header>
20
+ <div class="logo">
21
+ <a href="https://workadventu.re/" target="_blank" title="Workadventure">
22
+ <img src="public/images/logo.svg" alt="Workadventure logo" height="36" />
23
+ </a>
24
+ </div>
25
+ <div style="flex-grow: 1;"></div>
26
+ <div class="socials">
27
+ <a href="https://discord.gg/G6Xh9ZM9aR" target="_blank" title="discord">
28
+ <img src="/public/images/brand-discord.svg" alt="discord">
29
+ </a>
30
+ <a href="https://github.com/thecodingmachine/workadventure" target="_blank" title="github">
31
+ <img src="/public/images/brand-github.svg" alt="github">
32
+ </a>
33
+ <a href="https://www.youtube.com/channel/UCXJ9igV-kb9gw1ftR33y5tA" target="_blank" title="youtube">
34
+ <img src="/public/images/brand-youtube.svg" alt="youtube">
35
+ </a>
36
+ <a href="https://twitter.com/Workadventure_" target="_blank" title="twitter">
37
+ <img src="/public/images/brand-x.svg" alt="X">
38
+ </a>
39
+ <a href="https://www.linkedin.com/company/workadventu-re" target="_blank" title="linkedin">
40
+ <img src="/public/images/brand-linkedin.svg" alt="linkedin">
41
+ </a>
42
+ </div>
43
+ <div class="btn-header-wrapper">
44
+ <a href="#" class="btn btn-light">Talk to the community</a>
45
+ <a href="#" class="btn">Documentation</a>
46
+ </div>
47
+ <script type="module">
48
+ document.addEventListener("DOMContentLoaded", (event) => {
49
+ // Load index.js to have access to getMapsList
50
+ import('/public/assets/js/index.js').then((module) => {
51
+ module.createBackgroundImageFade();
52
+ });
53
+ });
54
+ </script>
55
+ </header>
56
+ <main>
57
+ <section class="form-center">
58
+ <h1>
59
+ Which version of WorkAdventure do you use?
60
+ </h1>
61
+ <div class="sub-heading">
62
+ Choose the setup that matches how your WorkAdventure instance is running. This will help us adapt the publishing process to your setup.
63
+ </div>
64
+ <div class="radio-wrapper">
65
+ <label class="radio-card">
66
+ <input name="advanced" class="radio" type="radio">
67
+ <div class="card-details">
68
+ <svg width="39" height="39" viewBox="0 0 39 39" fill="none" xmlns="http://www.w3.org/2000/svg">
69
+ <path d="M8.125 19.5L16.25 27.625L32.5 11.375" stroke="#4156F6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
70
+ </svg>
71
+
72
+ <h2>
73
+ I use self-hosted
74
+ </h2>
75
+ <div>
76
+ Your map is published on your own WorkAdventure instance.
77
+ <strong>You manage the hosting, configuration, and updates yourself.</strong>
78
+ </div>
79
+ </div>
80
+ </label>
81
+ <label class="radio-card">
82
+ <input name="advanced" class="radio" type="radio">
83
+ <div class="card-details">
84
+ <svg width="39" height="39" viewBox="0 0 39 39" fill="none" xmlns="http://www.w3.org/2000/svg">
85
+ <path d="M8.125 19.5L16.25 27.625L32.5 11.375" stroke="#4156F6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
86
+ </svg>
87
+
88
+ <h2>
89
+ I use SaaS version
90
+ </h2>
91
+ <div>
92
+ Your map is published on the WorkAdventure platform.
93
+ <strong>We handle hosting, updates, and infrastructure for you.</strong>
94
+ </div>
95
+ </div>
96
+ </label>
97
+ </div>
98
+
99
+ </section>
100
+ </main>
101
+ <div class="button-wrapper">
102
+ <div>
103
+ <a href="step1-git" class="btn btn-ghost">
104
+ Previous
105
+ </a>
106
+ </div>
107
+ <div style="flex-grow: 1;">
108
+ </div>
109
+ <div>
110
+ <a href="step3-steps" class="btn btn-secondary">
111
+ Configure
112
+ </a>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ <div class="bg"></div>
117
+ <script>
118
+ // Get radio buttons by finding the label containing "I use self-hosted"
119
+ const radioButtons = document.querySelectorAll('input[name="advanced"]');
120
+ const configureButton = document.querySelector('a[href="step3-steps"]');
121
+
122
+ // Find "I use self-hosted" radio button by checking parent label text
123
+ let selfHostedRadio = null;
124
+ radioButtons.forEach(radio => {
125
+ const label = radio.closest('label');
126
+ if (label && label.textContent.includes('I use self-hosted')) {
127
+ selfHostedRadio = radio;
128
+ }
129
+ });
130
+
131
+ // Function to update button href based on selection
132
+ function updateButtonHref() {
133
+ // Check if "I use self-hosted" is selected
134
+ if (selfHostedRadio && selfHostedRadio.checked) {
135
+ configureButton.href = "step3-steps-selfhosted";
136
+ configureButton.removeAttribute("target");
137
+ } else {
138
+ configureButton.href = "step3-steps";
139
+ configureButton.removeAttribute("target");
140
+ }
141
+ }
142
+
143
+ // Add event listeners to all radio buttons
144
+ radioButtons.forEach(radio => {
145
+ radio.addEventListener('change', updateButtonHref);
146
+ });
147
+
148
+ // Initialize on page load
149
+ updateButtonHref();
150
+ </script>
151
+ </body>
152
+
153
+ </html>
@@ -0,0 +1,502 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <meta name="robots" content="noindex">
9
+ <meta name="title" content="WorkAdventure Starter Kit - Self-hosted">
10
+
11
+ <link href="public/styles/styles.css" rel="stylesheet">
12
+
13
+ <title>WorkAdventure test map - Self-hosted</title>
14
+ <link rel="icon" href="public/images/favicon.svg" type="image/svg+xml">
15
+ <script type="module">
16
+ document.addEventListener("DOMContentLoaded", (event) => {
17
+ import('/public/assets/js/index.js').then((module) => {
18
+ module.createBackgroundImageFade();
19
+ });
20
+ });
21
+ </script>
22
+ </head>
23
+
24
+ <body>
25
+ <div class="content">
26
+ <header>
27
+ <div class="logo">
28
+ <a href="https://workadventu.re/" target="_blank" title="Workadventure">
29
+ <img src="public/images/logo.svg" alt="Workadventure logo" height="36" />
30
+ </a>
31
+ </div>
32
+ <div style="flex-grow: 1;"></div>
33
+ <div class="socials">
34
+ <a href="https://discord.gg/G6Xh9ZM9aR" target="_blank" title="discord">
35
+ <img src="/public/images/brand-discord.svg" alt="discord">
36
+ </a>
37
+ <a href="https://github.com/thecodingmachine/workadventure" target="_blank" title="github">
38
+ <img src="/public/images/brand-github.svg" alt="github">
39
+ </a>
40
+ <a href="https://www.youtube.com/channel/UCXJ9igV-kb9gw1ftR33y5tA" target="_blank" title="youtube">
41
+ <img src="/public/images/brand-youtube.svg" alt="youtube">
42
+ </a>
43
+ <a href="https://twitter.com/Workadventure_" target="_blank" title="twitter">
44
+ <img src="/public/images/brand-x.svg" alt="X">
45
+ </a>
46
+ <a href="https://www.linkedin.com/company/workadventu-re" target="_blank" title="linkedin">
47
+ <img src="/public/images/brand-linkedin.svg" alt="linkedin">
48
+ </a>
49
+ </div>
50
+ <div class="btn-header-wrapper">
51
+ <a href="https://discord.gg/G6Xh9ZM9aR" target="_blank" class="btn btn-light">Talk to the community</a>
52
+ <a href="https://docs.workadventu.re/map-building/" target="_blank" class="btn">Documentation</a>
53
+ </div>
54
+ </header>
55
+ <main class="container">
56
+ <section id="configureSteps" class="form-center steps">
57
+ <input type="hidden" id="stepProgress" value="1" />
58
+ <div id="step1" class="step">
59
+ <div class="step-title">
60
+ <div class="step-number">1</div>
61
+ <h2>Open your WorkAdventure self-hosted .env file</h2>
62
+ </div>
63
+ <div class="step-text">
64
+ In your WorkAdventure self-hosted installation directory, open the <code>.env</code> file. You will need the values of <strong>PUBLIC_MAP_STORAGE_URL</strong> and <strong>MAP_STORAGE_API_TOKEN</strong> from this file for the next steps.
65
+ </div>
66
+ <div style="margin-top: 16px;">
67
+ <button id="step1-done" class="btn">I have opened my .env file</button>
68
+ </div>
69
+ </div>
70
+
71
+ <div id="step2" class="step inactive">
72
+ <div class="step-title">
73
+ <div class="step-number">2</div>
74
+ <h2>Copy your PUBLIC_MAP_STORAGE_URL</h2>
75
+ </div>
76
+ <div class="step-text">
77
+ In your <code>.env</code> file, find the line <strong>PUBLIC_MAP_STORAGE_URL</strong>. Copy the value (the URL of your map storage service) and paste it below.
78
+ </div>
79
+ <div style="margin-top: 8px;">
80
+ <input id="mapStorageURL" type="url" placeholder="Paste here your MAP_STORAGE_URL..." />
81
+ </div>
82
+ </div>
83
+
84
+ <div id="step3" class="step inactive">
85
+ <div class="step-title">
86
+ <div class="step-number">3</div>
87
+ <h2>Copy your MAP_STORAGE_API_TOKEN</h2>
88
+ </div>
89
+ <div class="step-text">
90
+ In the same <code>.env</code> file, find the line <strong>MAP_STORAGE_API_TOKEN</strong>. Copy the token value and paste it below.
91
+ </div>
92
+ <div>
93
+ <input id="apiKey"
94
+ type="password"
95
+ placeholder="Paste here your MAP_STORAGE_API_TOKEN..."
96
+ autocomplete="off"
97
+ />
98
+ </div>
99
+ </div>
100
+
101
+ <div id="step4" class="step inactive">
102
+ <div class="step-title">
103
+ <div class="step-number">4</div>
104
+ <h2>Choose a name / directory for your world</h2>
105
+ </div>
106
+ <div class="step-text">
107
+ You can use an existing directory or create a new one. This is the directory where your maps will be stored on your map storage.
108
+ </div>
109
+ <div>
110
+ <input id="uploadDirectory" type="text" placeholder="Name of your world" />
111
+ </div>
112
+ </div>
113
+ </section>
114
+ </main>
115
+ <!-- Loading overlay -->
116
+ <div id="loadingOverlay" class="loading-overlay" style="display: none;">
117
+ <div class="loading-content">
118
+ <div class="loading-spinner"></div>
119
+ <p id="loadingText">Verifying your data...</p>
120
+ </div>
121
+ </div>
122
+ <!-- Error popup -->
123
+ <div id="errorPopup" class="error-popup" style="display: none;">
124
+ <div class="error-popup-content">
125
+ <div class="error-icon">
126
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
127
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
128
+ <path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
129
+ </svg>
130
+ </div>
131
+ <h3 class="error-title">Error</h3>
132
+ <p id="errorMessage" class="error-message"></p>
133
+ <button id="errorCloseButton" class="btn btn-secondary error-close-btn">Close</button>
134
+ </div>
135
+ </div>
136
+ <div class="button-wrapper">
137
+ <div>
138
+ <a href="step2-hosting" class="btn btn-ghost">
139
+ Previous
140
+ </a>
141
+ </div>
142
+ <div style="flex-grow: 1;">
143
+ </div>
144
+ <div>
145
+ <button id="saveButton" class="btn btn-secondary">
146
+ Upload
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ <div class="bg"></div>
152
+ <script>
153
+ let stepProgress = document.getElementById('stepProgress');
154
+ let step1 = document.getElementById('step1');
155
+ let step2 = document.getElementById('step2');
156
+ let step3 = document.getElementById('step3');
157
+ let step4 = document.getElementById('step4');
158
+ let mapStorageURL = document.getElementById('mapStorageURL');
159
+ let apiKey = document.getElementById('apiKey');
160
+ let uploadDirectory = document.getElementById('uploadDirectory');
161
+ const step1DoneButton = document.getElementById("step1-done");
162
+ const saveButton = document.getElementById("saveButton");
163
+ let formIsValid = false;
164
+
165
+ // For self-hosted: accept any valid HTTPS URL (not only workadventu.re)
166
+ function isValidMapStorageUrl(value) {
167
+ try {
168
+ const url = new URL(value);
169
+ return url.protocol === 'https:' || url.protocol === 'http:';
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ async function loadConfiguration() {
176
+ try {
177
+ const loadingOverlay = document.getElementById('loadingOverlay');
178
+ loadingOverlay.style.display = 'flex';
179
+
180
+ const loadingText = document.getElementById('loadingText');
181
+ loadingText.innerHTML = 'Verifying your data...';
182
+
183
+ const response = await fetch("/uploader/status");
184
+ if (!response.ok) {
185
+ throw new Error('Failed to load configuration status');
186
+ }
187
+
188
+ loadingText.innerHTML = 'Loading your configuration...';
189
+
190
+ const data = await response.json();
191
+
192
+ if (data.secretConfig) {
193
+ const config = data.secretConfig;
194
+ let currentStep = 1;
195
+
196
+ if (config.mapStorageUrl && isValidMapStorageUrl(config.mapStorageUrl)) {
197
+ mapStorageURL.value = config.mapStorageUrl;
198
+ mapStorageURL.classList.remove("error");
199
+ mapStorageURL.classList.add("success");
200
+ currentStep = 3;
201
+ step2.style.opacity = 1;
202
+ step2.classList.remove("inactive");
203
+ step3.style.opacity = 1;
204
+ step3.classList.remove("inactive");
205
+ formIsValid = 1;
206
+ }
207
+
208
+ if (config.mapStorageApiKey) {
209
+ apiKey.value = config.mapStorageApiKey;
210
+ apiKey.classList.remove("error");
211
+ apiKey.classList.add("success");
212
+ currentStep = 4;
213
+ step2.style.opacity = 1;
214
+ step2.classList.remove("inactive");
215
+ step3.style.opacity = 1;
216
+ step3.classList.remove("inactive");
217
+ step4.style.opacity = 1;
218
+ step4.classList.remove("inactive");
219
+ formIsValid = 2;
220
+ }
221
+
222
+ if (config.uploadDirectory) {
223
+ uploadDirectory.value = config.uploadDirectory;
224
+ if (config.uploadDirectory) {
225
+ uploadDirectory.classList.remove("error");
226
+ uploadDirectory.classList.add("success");
227
+ formIsValid = 3;
228
+ }
229
+ }
230
+
231
+ if (currentStep > 1) {
232
+ stepProgress.value = currentStep;
233
+ }
234
+
235
+ loadingOverlay.style.display = 'none';
236
+ } else {
237
+ loadingOverlay.style.display = 'none';
238
+ }
239
+ } catch (error) {
240
+ console.error('Error loading configuration:', error);
241
+ const loadingOverlay = document.getElementById('loadingOverlay');
242
+ loadingOverlay.style.display = 'none';
243
+ }
244
+ }
245
+
246
+ loadConfiguration();
247
+
248
+ step1DoneButton.addEventListener("click", (event) => {
249
+ stepProgress.value = 2;
250
+ step2.style.opacity = 1;
251
+ step2.classList.remove("inactive");
252
+ step2.scrollIntoView({ behavior: "smooth" });
253
+ });
254
+
255
+ mapStorageURL.addEventListener('input', function (evt) {
256
+ if (isValidMapStorageUrl(this.value)) {
257
+ mapStorageURL.classList.remove("error");
258
+ mapStorageURL.classList.add("success");
259
+ setTimeout(function () {
260
+ step3.style.opacity = 1;
261
+ step3.classList.remove("inactive");
262
+ step3.scrollIntoView({ behavior: "smooth" });
263
+ }, 500);
264
+ formIsValid++;
265
+ } else {
266
+ mapStorageURL.classList.remove("success");
267
+ mapStorageURL.classList.add("error");
268
+ formIsValid = false;
269
+ }
270
+ });
271
+
272
+ apiKey.addEventListener('input', function (evt) {
273
+ if (this.value) {
274
+ apiKey.classList.remove("error");
275
+ apiKey.classList.add("success");
276
+ setTimeout(function () {
277
+ step4.style.opacity = 1;
278
+ step4.classList.remove("inactive");
279
+ step4.scrollIntoView({ behavior: "smooth" });
280
+ formIsValid++;
281
+ }, 500);
282
+ } else {
283
+ apiKey.classList.remove("success");
284
+ apiKey.classList.add("error");
285
+ formIsValid = false;
286
+ }
287
+ });
288
+
289
+ uploadDirectory.addEventListener('input', function (evt) {
290
+ if (this.value) {
291
+ uploadDirectory.classList.remove("error");
292
+ uploadDirectory.classList.add("success");
293
+ formIsValid++;
294
+ } else {
295
+ uploadDirectory.classList.remove("success");
296
+ uploadDirectory.classList.add("error");
297
+ formIsValid = false;
298
+ }
299
+ });
300
+
301
+ function showErrorPopup(message) {
302
+ const errorPopup = document.getElementById('errorPopup');
303
+ const errorMessage = document.getElementById('errorMessage');
304
+ errorMessage.textContent = message;
305
+ errorPopup.style.display = 'flex';
306
+ }
307
+
308
+ function hideErrorPopup() {
309
+ const errorPopup = document.getElementById('errorPopup');
310
+ errorPopup.style.display = 'none';
311
+ }
312
+
313
+ document.getElementById('errorCloseButton').addEventListener('click', hideErrorPopup);
314
+
315
+ document.getElementById('errorPopup').addEventListener('click', function (e) {
316
+ if (e.target === this) {
317
+ hideErrorPopup();
318
+ }
319
+ });
320
+
321
+ const loadingText = document.getElementById('loadingText');
322
+
323
+ saveButton.addEventListener("click", async (event) => {
324
+ if (!mapStorageURL.value || !isValidMapStorageUrl(mapStorageURL.value) ||
325
+ !apiKey.value || !uploadDirectory.value) {
326
+ showErrorPopup('Please fill in all required fields correctly.');
327
+ return;
328
+ }
329
+
330
+ const loadingOverlay = document.getElementById('loadingOverlay');
331
+ loadingOverlay.style.display = 'flex';
332
+
333
+ const startTime = Date.now();
334
+ const minDuration = 2000;
335
+
336
+ try {
337
+ loadingText.innerHTML = 'Saving your configuration...';
338
+
339
+ const configureResponse = await fetch("/uploader/configure", {
340
+ method: "POST",
341
+ headers: {
342
+ "Content-Type": "application/json"
343
+ },
344
+ body: JSON.stringify({
345
+ mapStorageUrl: mapStorageURL.value,
346
+ mapStorageApiKey: apiKey.value,
347
+ uploadDirectory: uploadDirectory.value
348
+ })
349
+ });
350
+
351
+ if (!configureResponse.ok) {
352
+ const errorData = await configureResponse.json().catch(() => ({}));
353
+ throw new Error(errorData.message || errorData.error || 'Failed to save configuration. Please check your inputs and try again.');
354
+ }
355
+
356
+ loadingText.innerHTML = 'Uploading your map...';
357
+
358
+ const uploadResponse = await fetch("/uploader/upload", {
359
+ method: "POST",
360
+ headers: {
361
+ "Content-Type": "application/json"
362
+ }
363
+ });
364
+
365
+ if (!uploadResponse.ok) {
366
+ const errorData = await uploadResponse.json().catch(() => ({}));
367
+ const errorMsg = errorData.message || errorData.error || 'Failed to upload map. Please verify your configuration and try again.';
368
+ throw new Error(errorMsg);
369
+ }
370
+
371
+ const elapsed = Date.now() - startTime;
372
+ if (elapsed < minDuration) {
373
+ await new Promise(resolve => setTimeout(resolve, minDuration - elapsed));
374
+ }
375
+
376
+ window.location.href = "/step4-validated-selfhosted";
377
+ } catch (error) {
378
+ console.error('Error saving configuration:', error);
379
+ loadingOverlay.style.display = 'none';
380
+
381
+ const errorMessage = error instanceof Error ? error.message : 'An error occurred while saving your configuration. Please try again.';
382
+ showErrorPopup(errorMessage);
383
+ }
384
+ });
385
+ </script>
386
+ <style>
387
+ .loading-overlay {
388
+ position: fixed;
389
+ top: 0;
390
+ left: 0;
391
+ width: 100%;
392
+ height: 100%;
393
+ background-color: rgba(27, 42, 65, 0.9);
394
+ display: flex;
395
+ justify-content: center;
396
+ align-items: center;
397
+ z-index: 9999;
398
+ }
399
+
400
+ .loading-content {
401
+ text-align: center;
402
+ color: white;
403
+ }
404
+
405
+ .loading-spinner {
406
+ border: 4px solid rgba(255, 255, 255, 0.3);
407
+ border-top: 4px solid #66E979;
408
+ border-radius: 50%;
409
+ width: 50px;
410
+ height: 50px;
411
+ animation: spin 1s linear infinite;
412
+ margin: 0 auto 20px;
413
+ }
414
+
415
+ @keyframes spin {
416
+ 0% { transform: rotate(0deg); }
417
+ 100% { transform: rotate(360deg); }
418
+ }
419
+
420
+ .loading-content p {
421
+ margin: 0;
422
+ font-size: 18px;
423
+ font-weight: 500;
424
+ }
425
+
426
+ .error-popup {
427
+ position: fixed;
428
+ top: 0;
429
+ left: 0;
430
+ width: 100%;
431
+ height: 100%;
432
+ background-color: rgba(27, 42, 65, 0.85);
433
+ display: flex;
434
+ justify-content: center;
435
+ align-items: center;
436
+ z-index: 10000;
437
+ animation: fadeIn 0.3s ease-in-out;
438
+ }
439
+
440
+ @keyframes fadeIn {
441
+ from { opacity: 0; }
442
+ to { opacity: 1; }
443
+ }
444
+
445
+ .error-popup-content {
446
+ background: rgba(27, 42, 65, 0.95);
447
+ border: 2px solid #ff4444;
448
+ border-radius: 16px;
449
+ padding: 32px;
450
+ max-width: 500px;
451
+ width: 90%;
452
+ text-align: center;
453
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
454
+ animation: slideUp 0.3s ease-out;
455
+ }
456
+
457
+ @keyframes slideUp {
458
+ from { transform: translateY(20px); opacity: 0; }
459
+ to { transform: translateY(0); opacity: 1; }
460
+ }
461
+
462
+ .error-icon {
463
+ color: #ff4444;
464
+ margin-bottom: 16px;
465
+ display: flex;
466
+ justify-content: center;
467
+ }
468
+
469
+ .error-icon svg {
470
+ width: 64px;
471
+ height: 64px;
472
+ }
473
+
474
+ .error-title {
475
+ color: #ff4444;
476
+ font-size: 24px;
477
+ font-weight: bold;
478
+ margin: 0 0 16px 0;
479
+ }
480
+
481
+ .error-message {
482
+ color: #ffffff;
483
+ font-size: 16px;
484
+ line-height: 1.5;
485
+ margin: 0 0 24px 0;
486
+ }
487
+
488
+ .error-close-btn {
489
+ margin-top: 8px;
490
+ min-width: 120px;
491
+ }
492
+
493
+ .step-text code {
494
+ background: rgba(255, 255, 255, 0.1);
495
+ padding: 2px 6px;
496
+ border-radius: 4px;
497
+ font-family: monospace;
498
+ }
499
+ </style>
500
+ </body>
501
+
502
+ </html>