cloudfrontize 1.2.0 → 1.3.1
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.
- package/README.md +65 -22
- package/dist/cli.js +36 -32
- package/dist/ui/app.js +442 -0
- package/dist/ui/assets/cloudfrontize-pro-transparent-512.png +0 -0
- package/dist/ui/favicons/android-icon-144x144.png +0 -0
- package/dist/ui/favicons/android-icon-192x192.png +0 -0
- package/dist/ui/favicons/android-icon-36x36.png +0 -0
- package/dist/ui/favicons/android-icon-48x48.png +0 -0
- package/dist/ui/favicons/android-icon-72x72.png +0 -0
- package/dist/ui/favicons/android-icon-96x96.png +0 -0
- package/dist/ui/favicons/apple-icon-114x114.png +0 -0
- package/dist/ui/favicons/apple-icon-120x120.png +0 -0
- package/dist/ui/favicons/apple-icon-144x144.png +0 -0
- package/dist/ui/favicons/apple-icon-152x152.png +0 -0
- package/dist/ui/favicons/apple-icon-180x180.png +0 -0
- package/dist/ui/favicons/apple-icon-57x57.png +0 -0
- package/dist/ui/favicons/apple-icon-60x60.png +0 -0
- package/dist/ui/favicons/apple-icon-72x72.png +0 -0
- package/dist/ui/favicons/apple-icon-76x76.png +0 -0
- package/dist/ui/favicons/apple-icon-precomposed.png +0 -0
- package/dist/ui/favicons/apple-icon.png +0 -0
- package/dist/ui/favicons/browserconfig.xml +11 -0
- package/dist/ui/favicons/favicon-16x16.png +0 -0
- package/dist/ui/favicons/favicon-32x32.png +0 -0
- package/dist/ui/favicons/favicon-96x96.png +0 -0
- package/dist/ui/favicons/favicon.ico +0 -0
- package/dist/ui/favicons/manifest.json +41 -0
- package/dist/ui/favicons/ms-icon-144x144.png +0 -0
- package/dist/ui/favicons/ms-icon-150x150.png +0 -0
- package/dist/ui/favicons/ms-icon-310x310.png +0 -0
- package/dist/ui/favicons/ms-icon-70x70.png +0 -0
- package/dist/ui/index.html +129 -0
- package/dist/ui/style.css +792 -0
- package/package.json +1 -1
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CloudFrontize Developer UI - Client Logic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
8
|
+
const requestFeed = document.getElementById('request-feed');
|
|
9
|
+
const portDisplay = document.getElementById('current-port');
|
|
10
|
+
|
|
11
|
+
// Tab Switching
|
|
12
|
+
const tabBtns = document.querySelectorAll('.tab-btn');
|
|
13
|
+
const tabContents = document.querySelectorAll('.tab-content');
|
|
14
|
+
|
|
15
|
+
tabBtns.forEach(btn => {
|
|
16
|
+
btn.addEventListener('click', () => {
|
|
17
|
+
tabBtns.forEach(b => b.classList.remove('active'));
|
|
18
|
+
tabContents.forEach(c => c.classList.remove('active'));
|
|
19
|
+
|
|
20
|
+
btn.classList.add('active');
|
|
21
|
+
document.getElementById(btn.dataset.tab).classList.add('active');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Filtering Pulse
|
|
26
|
+
const filterBtns = document.querySelectorAll('.btn-filter');
|
|
27
|
+
filterBtns.forEach(btn => {
|
|
28
|
+
btn.addEventListener('click', () => {
|
|
29
|
+
filterBtns.forEach(b => b.classList.remove('active'));
|
|
30
|
+
btn.classList.add('active');
|
|
31
|
+
|
|
32
|
+
const type = btn.dataset.type;
|
|
33
|
+
const cards = document.querySelectorAll('.request-card');
|
|
34
|
+
cards.forEach(card => {
|
|
35
|
+
if (!type || type === 'all') {
|
|
36
|
+
card.style.display = 'block';
|
|
37
|
+
} else if (type === 'rewrite') {
|
|
38
|
+
card.style.display = card.classList.contains('is-rewrite') ? 'block' : 'none';
|
|
39
|
+
} else if (type === 'violation') {
|
|
40
|
+
card.style.display = card.classList.contains('has-violation') ? 'block' : 'none';
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Real-time Feed via SSE
|
|
47
|
+
const evtSource = new EventSource('/events');
|
|
48
|
+
|
|
49
|
+
evtSource.onmessage = (event) => {
|
|
50
|
+
const data = JSON.parse(event.data);
|
|
51
|
+
if (data.type === 'init') {
|
|
52
|
+
portDisplay.textContent = data.port;
|
|
53
|
+
|
|
54
|
+
// Dynamic Versioning
|
|
55
|
+
const versionTag = document.querySelector('.version-tag');
|
|
56
|
+
if (versionTag && data.version) {
|
|
57
|
+
versionTag.textContent = `Developer Edition v${data.version}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wipe all local state for a clean slate
|
|
61
|
+
requestFeed.innerHTML = '<div class="empty-state"><p>Waiting for requests...</p></div>';
|
|
62
|
+
loadHeaderState(data.headerState);
|
|
63
|
+
|
|
64
|
+
// Reset "Dirty" state
|
|
65
|
+
markClean();
|
|
66
|
+
|
|
67
|
+
// Reset filters to "All"
|
|
68
|
+
const allFilterBtn = document.querySelector('.btn-filter[data-type="all"]');
|
|
69
|
+
if (allFilterBtn) {
|
|
70
|
+
filterBtns.forEach(b => b.classList.remove('active'));
|
|
71
|
+
allFilterBtn.classList.add('active');
|
|
72
|
+
}
|
|
73
|
+
} else if (data.type === 'request') {
|
|
74
|
+
addRequestToFeed(data.request);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
evtSource.onerror = () => {
|
|
79
|
+
console.error("SSE Connection lost.");
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const sidebar = document.querySelector('.sidebar');
|
|
83
|
+
const applyBtn = document.getElementById('apply-headers');
|
|
84
|
+
let isDirty = false;
|
|
85
|
+
|
|
86
|
+
function markDirty() {
|
|
87
|
+
if (isDirty) return;
|
|
88
|
+
isDirty = true;
|
|
89
|
+
applyBtn.classList.add('dirty');
|
|
90
|
+
applyBtn.innerText = 'Save Changes';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function markClean() {
|
|
94
|
+
isDirty = false;
|
|
95
|
+
applyBtn.classList.remove('dirty');
|
|
96
|
+
applyBtn.innerText = 'Applied';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Header Management
|
|
100
|
+
function createHeaderRow(key = '', value = '', isDelete = false) {
|
|
101
|
+
const row = document.createElement('div');
|
|
102
|
+
row.className = 'header-row' + (isDelete ? ' suppressed' : '');
|
|
103
|
+
row.innerHTML = `
|
|
104
|
+
<div class="row-main">
|
|
105
|
+
<input type="text" placeholder="Header Name" value="${key}" class="hdr-key">
|
|
106
|
+
<input type="text" placeholder="Value" value="${value}" class="hdr-val" ${isDelete ? 'disabled' : ''}>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="row-actions">
|
|
109
|
+
<button class="btn-toggle-action ${isDelete ? '' : 'active'}" title="${isDelete ? 'Set: Override/Inject' : 'Suppress: Explicitly Delete'}">
|
|
110
|
+
<span class="material-icons">${isDelete ? 'do_not_disturb_on' : 'verified'}</span>
|
|
111
|
+
</button>
|
|
112
|
+
<button class="btn-remove" title="Remove"><span class="material-icons">delete</span></button>
|
|
113
|
+
</div>
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const actionBtn = row.querySelector('.btn-toggle-action');
|
|
117
|
+
const valInput = row.querySelector('.hdr-val');
|
|
118
|
+
const keyInput = row.querySelector('.hdr-key');
|
|
119
|
+
|
|
120
|
+
const handleChange = () => markDirty();
|
|
121
|
+
keyInput.oninput = handleChange;
|
|
122
|
+
valInput.oninput = handleChange;
|
|
123
|
+
|
|
124
|
+
actionBtn.onclick = () => {
|
|
125
|
+
const nowActive = actionBtn.classList.toggle('active');
|
|
126
|
+
const isSuppressed = !nowActive;
|
|
127
|
+
|
|
128
|
+
row.classList.toggle('suppressed', isSuppressed);
|
|
129
|
+
actionBtn.title = isSuppressed ? 'Set: Override/Inject' : 'Suppress: Explicitly Delete';
|
|
130
|
+
actionBtn.innerHTML = `<span class="material-icons">${isSuppressed ? 'do_not_disturb_on' : 'verified'}</span>`;
|
|
131
|
+
valInput.disabled = isSuppressed;
|
|
132
|
+
markDirty();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
row.querySelector('.btn-remove').onclick = () => {
|
|
136
|
+
row.remove();
|
|
137
|
+
markDirty();
|
|
138
|
+
};
|
|
139
|
+
return row;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
document.getElementById('add-req-header').onclick = () => {
|
|
143
|
+
document.getElementById('req-header-list').appendChild(createHeaderRow());
|
|
144
|
+
markDirty();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
document.getElementById('add-res-header').onclick = () => {
|
|
148
|
+
document.getElementById('res-header-list').appendChild(createHeaderRow());
|
|
149
|
+
markDirty();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Presets
|
|
153
|
+
document.querySelectorAll('.btn-preset').forEach(btn => {
|
|
154
|
+
btn.onclick = () => {
|
|
155
|
+
const listId = btn.dataset.type === 'request' ? 'req-header-list' : 'res-header-list';
|
|
156
|
+
const list = document.getElementById(listId);
|
|
157
|
+
list.appendChild(createHeaderRow(btn.dataset.header, btn.dataset.value));
|
|
158
|
+
markDirty();
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
document.getElementById('reset-headers').onclick = () => {
|
|
163
|
+
if (confirm('Reset all overrides to file defaults?')) {
|
|
164
|
+
loadHeaderState({}); // We would fetch defaults if we had them saved, but for now we clear
|
|
165
|
+
markDirty();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Save State
|
|
170
|
+
applyBtn.onclick = async () => {
|
|
171
|
+
const state = { request: {}, response: {} };
|
|
172
|
+
let hasValidationError = false;
|
|
173
|
+
|
|
174
|
+
const collect = (listId, target) => {
|
|
175
|
+
document.querySelectorAll(`#${listId} .header-row`).forEach(row => {
|
|
176
|
+
const keyInput = row.querySelector('.hdr-key');
|
|
177
|
+
const valInput = row.querySelector('.hdr-val');
|
|
178
|
+
const k = keyInput.value.trim();
|
|
179
|
+
const v = valInput.value.trim();
|
|
180
|
+
const isSuppressed = row.classList.contains('suppressed');
|
|
181
|
+
|
|
182
|
+
if (!k) {
|
|
183
|
+
// Any dangling row must have a name, or it's a validation error
|
|
184
|
+
keyInput.classList.add('input-error');
|
|
185
|
+
hasValidationError = true;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
keyInput.classList.remove('input-error');
|
|
190
|
+
target[k] = isSuppressed ? null : v;
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
collect('req-header-list', state.request);
|
|
195
|
+
collect('res-header-list', state.response);
|
|
196
|
+
|
|
197
|
+
if (hasValidationError) {
|
|
198
|
+
applyBtn.innerText = '🛑 Fix Header Names';
|
|
199
|
+
applyBtn.classList.add('error-pulse');
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
applyBtn.innerText = isDirty ? 'Save Changes' : 'Applied';
|
|
202
|
+
applyBtn.classList.remove('error-pulse');
|
|
203
|
+
}, 3000);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const res = await fetch('/headers', {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify(state)
|
|
212
|
+
});
|
|
213
|
+
if (res.ok) {
|
|
214
|
+
markClean();
|
|
215
|
+
applyBtn.innerText = '✅ Applied';
|
|
216
|
+
applyBtn.classList.add('success');
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
applyBtn.innerText = 'Applied';
|
|
219
|
+
applyBtn.classList.remove('success');
|
|
220
|
+
}, 2000);
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
alert('🛑 Failed to save headers.');
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function loadHeaderState(state) {
|
|
228
|
+
const reqList = document.getElementById('req-header-list');
|
|
229
|
+
const resList = document.getElementById('res-header-list');
|
|
230
|
+
reqList.innerHTML = '';
|
|
231
|
+
resList.innerHTML = '';
|
|
232
|
+
|
|
233
|
+
Object.entries(state.request || {}).forEach(([k, v]) => reqList.appendChild(createHeaderRow(k, v, v === null)));
|
|
234
|
+
Object.entries(state.response || {}).forEach(([k, v]) => resList.appendChild(createHeaderRow(k, v, v === null)));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function addRequestToFeed(req) {
|
|
238
|
+
if (document.querySelector('.empty-state')) {
|
|
239
|
+
requestFeed.innerHTML = '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const card = document.createElement('div');
|
|
243
|
+
card.className = 'request-card';
|
|
244
|
+
card.id = `req-${req.id}`;
|
|
245
|
+
if (req.violation) card.classList.add('has-violation');
|
|
246
|
+
if (req.steps && req.steps.length > 1) card.classList.add('is-rewrite');
|
|
247
|
+
|
|
248
|
+
const statusClass = req.status >= 500 ? 'status-error' : (req.status >= 400 ? 'status-warn' : 'status-ok');
|
|
249
|
+
|
|
250
|
+
card.innerHTML = `
|
|
251
|
+
<div class="card-header">
|
|
252
|
+
<div style="display: flex; align-items: center; gap: 0.75rem">
|
|
253
|
+
<span class="card-id">#${req.id.slice(0, 8)}</span>
|
|
254
|
+
<span class="card-method">${req.method}</span>
|
|
255
|
+
<span class="card-uri">${req.path}</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div style="display: flex; align-items: center; gap: 1rem">
|
|
258
|
+
<span class="card-status ${statusClass}">${req.status}</span>
|
|
259
|
+
<span class="expand-icon">expand_more</span>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="card-body-mini">
|
|
263
|
+
<div class="rewrite-path">${req.steps.length > 1 ? `↳ ${req.steps[req.steps.length - 1].uri}` : ''}</div>
|
|
264
|
+
<div class="performance-mini">${req.cpu.toFixed(2)}ms</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="card-details" style="display: none;">
|
|
267
|
+
<div class="loading-spinner">Loading deep inspection data...</div>
|
|
268
|
+
</div>
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
card.querySelector('.expand-icon').onclick = (e) => {
|
|
272
|
+
e.stopPropagation();
|
|
273
|
+
toggleRequestDetails(req.id, card);
|
|
274
|
+
};
|
|
275
|
+
requestFeed.prepend(card);
|
|
276
|
+
|
|
277
|
+
if (requestFeed.children.length > 50) {
|
|
278
|
+
requestFeed.removeChild(requestFeed.lastChild);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function toggleRequestDetails(id, card) {
|
|
283
|
+
const detailsEl = card.querySelector('.card-details');
|
|
284
|
+
const iconEl = card.querySelector('.expand-icon');
|
|
285
|
+
const isExpanded = detailsEl.style.display === 'block';
|
|
286
|
+
|
|
287
|
+
if (isExpanded) {
|
|
288
|
+
detailsEl.style.display = 'none';
|
|
289
|
+
card.classList.remove('expanded');
|
|
290
|
+
iconEl.innerText = 'expand_more';
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
detailsEl.style.display = 'block';
|
|
295
|
+
card.classList.add('expanded');
|
|
296
|
+
iconEl.innerText = 'expand_less';
|
|
297
|
+
|
|
298
|
+
if (detailsEl.innerHTML.includes('Loading deep inspection data...')) {
|
|
299
|
+
try {
|
|
300
|
+
const res = await fetch(`/request/${id}`);
|
|
301
|
+
const data = await res.json();
|
|
302
|
+
renderDetails(detailsEl, data);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
detailsEl.innerHTML = `<div class="error">Failed to load details: ${err.message}</div>`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderDetails(el, data) {
|
|
310
|
+
el.innerHTML = `
|
|
311
|
+
<div class="detail-nav">
|
|
312
|
+
<div class="nav-item active" data-pane="trace">Trace</div>
|
|
313
|
+
<div class="nav-item" data-pane="headers">Headers</div>
|
|
314
|
+
<div class="nav-item" data-pane="body">Body</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div class="detail-pane active" id="pane-trace-${data.id}">
|
|
318
|
+
<div class="detail-grid">
|
|
319
|
+
<div class="detail-section">
|
|
320
|
+
<h4>Pipeline Evolution</h4>
|
|
321
|
+
<div class="shadow-rewrite">
|
|
322
|
+
${data.steps.map(s => `
|
|
323
|
+
<div class="step">
|
|
324
|
+
<span>${s.uri}</span>
|
|
325
|
+
<span class="step-label">${s.label}</span>
|
|
326
|
+
</div>
|
|
327
|
+
`).join('')}
|
|
328
|
+
</div>
|
|
329
|
+
${data.violation ? `
|
|
330
|
+
<div class="fidelity-alert">
|
|
331
|
+
<span>🛑</span>
|
|
332
|
+
<div>
|
|
333
|
+
<strong>Fidelity Violation:</strong> ${data.violation}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
` : ''}
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div class="detail-pane" id="pane-headers-${data.id}">
|
|
342
|
+
<div class="header-lifecycle">
|
|
343
|
+
<div class="lifecycle-col">
|
|
344
|
+
<h5>Request lifecycle (Viewer → Origin)</h5>
|
|
345
|
+
<div class="header-diff">
|
|
346
|
+
${renderHeaderDiff(data.headers.request.viewer, data.headers.request.origin)}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="lifecycle-col">
|
|
350
|
+
<h5>Response lifecycle (Origin → Viewer)</h5>
|
|
351
|
+
<div class="header-diff">
|
|
352
|
+
${renderHeaderDiff(data.headers.response.origin, data.headers.response.viewer)}
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div class="detail-pane" id="pane-body-${data.id}">
|
|
359
|
+
<div class="detail-section">
|
|
360
|
+
<h4>Body Snippet (First 1KB)</h4>
|
|
361
|
+
<pre class="body-snippet">${data.bodySnippet || '(No body content buffered)'}</pre>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
`;
|
|
365
|
+
|
|
366
|
+
// Wire up internal tabs
|
|
367
|
+
el.querySelectorAll('.nav-item').forEach(nav => {
|
|
368
|
+
nav.onclick = (e) => {
|
|
369
|
+
e.stopPropagation();
|
|
370
|
+
el.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
371
|
+
el.querySelectorAll('.detail-pane').forEach(p => p.classList.remove('active'));
|
|
372
|
+
|
|
373
|
+
nav.classList.add('active');
|
|
374
|
+
el.querySelector(`#pane-${nav.dataset.pane}-${data.id}`).classList.add('active');
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function renderHeaderDiff(incoming, final) {
|
|
380
|
+
const allKeys = new Set([...Object.keys(incoming), ...Object.keys(final)]);
|
|
381
|
+
let html = '<table class="diff-table">';
|
|
382
|
+
|
|
383
|
+
[...allKeys].sort().forEach(key => {
|
|
384
|
+
const val1 = incoming[key];
|
|
385
|
+
const val2 = final[key];
|
|
386
|
+
|
|
387
|
+
let rowClass = '';
|
|
388
|
+
if (val1 === undefined) rowClass = 'h-added';
|
|
389
|
+
else if (val2 === undefined) rowClass = 'h-removed';
|
|
390
|
+
else if (JSON.stringify(val1) !== JSON.stringify(val2)) rowClass = 'h-modified';
|
|
391
|
+
|
|
392
|
+
html += `
|
|
393
|
+
<tr class="${rowClass}">
|
|
394
|
+
<td class="h-key">${key}</td>
|
|
395
|
+
<td class="h-val">${val2 !== undefined ? val2 : '<span class="deleted">REMOVED</span>'}</td>
|
|
396
|
+
</tr>
|
|
397
|
+
`;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return html + '</table>';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
document.getElementById('clear-feed').onclick = () => {
|
|
404
|
+
requestFeed.innerHTML = '<div class="empty-state"><p>Waiting for requests...</p></div>';
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// About Modal Logic
|
|
408
|
+
const aboutModal = document.getElementById('about-modal');
|
|
409
|
+
const aboutTrigger = document.getElementById('about-trigger');
|
|
410
|
+
const closeAbout = document.getElementById('close-about');
|
|
411
|
+
|
|
412
|
+
const showAbout = () => {
|
|
413
|
+
aboutModal.classList.add('active');
|
|
414
|
+
document.body.style.overflow = 'hidden'; // Prevent background scroll
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const hideAbout = () => {
|
|
418
|
+
aboutModal.classList.remove('active');
|
|
419
|
+
document.body.style.overflow = '';
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
aboutTrigger.addEventListener('click', (e) => {
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
showAbout();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
closeAbout.addEventListener('click', hideAbout);
|
|
428
|
+
|
|
429
|
+
// Close on click outside
|
|
430
|
+
aboutModal.addEventListener('click', (e) => {
|
|
431
|
+
if (e.target === aboutModal) {
|
|
432
|
+
hideAbout();
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Close on Escape
|
|
437
|
+
document.addEventListener('keydown', (e) => {
|
|
438
|
+
if (e.key === 'Escape' && aboutModal.classList.contains('active')) {
|
|
439
|
+
hideAbout();
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<browserconfig>
|
|
3
|
+
<msapplication>
|
|
4
|
+
<tile>
|
|
5
|
+
<square70x70logo src="/favicons/ms-icon-70x70.png"/>
|
|
6
|
+
<square150x150logo src="/favicons/ms-icon-150x150.png"/>
|
|
7
|
+
<square310x310logo src="/favicons/ms-icon-310x310.png"/>
|
|
8
|
+
<TileColor>#ffffff</TileColor>
|
|
9
|
+
</tile>
|
|
10
|
+
</msapplication>
|
|
11
|
+
</browserconfig>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "App",
|
|
3
|
+
"icons": [
|
|
4
|
+
{
|
|
5
|
+
"src": "/favicons/android-icon-36x36.png",
|
|
6
|
+
"sizes": "36x36",
|
|
7
|
+
"type": "image/png",
|
|
8
|
+
"density": "0.75"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "/favicons/android-icon-48x48.png",
|
|
12
|
+
"sizes": "48x48",
|
|
13
|
+
"type": "image/png",
|
|
14
|
+
"density": "1.0"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"src": "/favicons/android-icon-72x72.png",
|
|
18
|
+
"sizes": "72x72",
|
|
19
|
+
"type": "image/png",
|
|
20
|
+
"density": "1.5"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"src": "/favicons/android-icon-96x96.png",
|
|
24
|
+
"sizes": "96x96",
|
|
25
|
+
"type": "image/png",
|
|
26
|
+
"density": "2.0"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"src": "/favicons/android-icon-144x144.png",
|
|
30
|
+
"sizes": "144x144",
|
|
31
|
+
"type": "image/png",
|
|
32
|
+
"density": "3.0"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"src": "/favicons/android-icon-192x192.png",
|
|
36
|
+
"sizes": "192x192",
|
|
37
|
+
"type": "image/png",
|
|
38
|
+
"density": "4.0"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|