zero-http 0.2.1 → 0.2.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.
- package/documentation/full-server.js +21 -5
- package/documentation/public/index.html +30 -37
- package/documentation/public/scripts/data-sections.js +19 -0
- package/documentation/public/scripts/playground.js +4 -1
- package/documentation/public/scripts/ui.js +135 -3
- package/documentation/public/scripts/uploads.js +26 -11
- package/documentation/public/styles.css +35 -12
- package/documentation/public/vendor/icons/compress.svg +6 -6
- package/documentation/public/vendor/icons/https.svg +0 -5
- package/documentation/public/vendor/icons/logo.svg +24 -0
- package/documentation/public/vendor/icons/router.svg +0 -2
- package/documentation/public/vendor/icons/sse.svg +0 -3
- package/lib/app.js +12 -1
- package/package.json +1 -1
|
@@ -173,20 +173,36 @@ apiRouter.get('/info', (req, res) => res.json({
|
|
|
173
173
|
app.use('/api', apiRouter);
|
|
174
174
|
|
|
175
175
|
// --- Route introspection ---
|
|
176
|
-
app.get('/debug/routes', (req, res) =>
|
|
176
|
+
app.get('/debug/routes', (req, res) =>
|
|
177
|
+
{
|
|
178
|
+
res.set('Content-Type', 'application/json');
|
|
179
|
+
res.send(JSON.stringify(app.routes(), null, 2));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- TLS Certificates (HTTPS + WSS) ---
|
|
183
|
+
const certPath = '/www/server/panel/vhost/cert/zero-http.molex.cloud/fullchain.pem';
|
|
184
|
+
const keyPath = '/www/server/panel/vhost/cert/zero-http.molex.cloud/privkey.pem';
|
|
185
|
+
const hasCerts = fs.existsSync(certPath) && fs.existsSync(keyPath);
|
|
186
|
+
|
|
187
|
+
const tlsOpts = hasCerts
|
|
188
|
+
? { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) }
|
|
189
|
+
: undefined;
|
|
177
190
|
|
|
178
191
|
// --- Server Startup ---
|
|
179
|
-
const port = process.env.PORT ||
|
|
180
|
-
const server = app.listen(port, () =>
|
|
192
|
+
const port = process.env.PORT || 7273;
|
|
193
|
+
const server = app.listen(port, tlsOpts, () =>
|
|
181
194
|
{
|
|
182
|
-
|
|
195
|
+
const proto = hasCerts ? 'https' : 'http';
|
|
196
|
+
console.log(`zero-http full-server listening on ${proto}://localhost:${port}`);
|
|
183
197
|
if (process.argv.includes('--test')) runTests(port).catch(console.error);
|
|
184
198
|
});
|
|
185
199
|
|
|
200
|
+
|
|
186
201
|
/** Quick smoke tests using built-in fetch */
|
|
187
202
|
async function runTests(port)
|
|
188
203
|
{
|
|
189
|
-
const
|
|
204
|
+
const proto = hasCerts ? 'https' : 'http';
|
|
205
|
+
const base = `${proto}://localhost:${port}`;
|
|
190
206
|
console.log('running smoke tests against', base);
|
|
191
207
|
|
|
192
208
|
const doReq = async (label, promise) =>
|
|
@@ -71,20 +71,24 @@
|
|
|
71
71
|
<aside class="toc-sidebar" aria-label="Table of contents">
|
|
72
72
|
<nav>
|
|
73
73
|
<div class="toc-toolbar">
|
|
74
|
+
<div class="toc-search-wrap">
|
|
75
|
+
<svg class="toc-search-icon" width="13" height="13" viewBox="0 0 16 16" fill="none"><circle cx="6.5" cy="6.5" r="5.5" stroke="currentColor" stroke-width="2"/><path d="M11 11l4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
76
|
+
<input id="toc-search" class="toc-search" type="text" placeholder="Filter…" autocomplete="off" spellcheck="false" />
|
|
77
|
+
</div>
|
|
78
|
+
<button class="toc-tool-btn" id="toc-toggle-acc" title="Collapse all" aria-label="Collapse all sections">
|
|
79
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
80
|
+
</button>
|
|
74
81
|
<button class="toc-tool-btn" id="toc-top-btn" title="Back to top" aria-label="Scroll to top">
|
|
75
82
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 13V3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M3 7l5-5 5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
76
83
|
</button>
|
|
77
|
-
<button class="toc-tool-btn" id="toc-toggle-acc" title="Expand all" aria-label="Expand all sections">
|
|
78
|
-
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 9l6 4 6-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
79
|
-
</button>
|
|
80
84
|
</div>
|
|
81
85
|
<ul>
|
|
82
86
|
<li><a href="#features">Features</a></li>
|
|
83
87
|
<li><a href="#quickstart">Quickstart</a></li>
|
|
84
88
|
<li><a href="#options">Options</a></li>
|
|
85
|
-
<li><a href="#api-reference">API Reference</a></li>
|
|
86
|
-
<li><a href="#simple-examples">Simple Examples</a></li>
|
|
87
|
-
<li><a href="#playground">Playground</a>
|
|
89
|
+
<li class="toc-collapsible"><a href="#api-reference">API Reference</a></li>
|
|
90
|
+
<li class="toc-collapsible"><a href="#simple-examples">Simple Examples</a></li>
|
|
91
|
+
<li class="toc-collapsible"><a href="#playground">Playground</a>
|
|
88
92
|
<ul class="toc-sub">
|
|
89
93
|
<li class="toc-sub-item"><a href="#upload-testing">Upload multipart data</a></li>
|
|
90
94
|
<li class="toc-sub-item"><a href="#parser-tests">Parser tests</a></li>
|
|
@@ -192,40 +196,29 @@
|
|
|
192
196
|
</div>
|
|
193
197
|
</div>
|
|
194
198
|
<div id="tab-behavior" class="tab-panel">
|
|
195
|
-
<div class="
|
|
196
|
-
<div class="
|
|
197
|
-
<div class="
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
<p>Routing and middleware run in the order they're registered—add parsers before
|
|
201
|
-
handlers.</p>
|
|
202
|
-
</div>
|
|
199
|
+
<div class="pipeline">
|
|
200
|
+
<div class="pipe-stage">
|
|
201
|
+
<div class="pipe-icon"><svg viewBox="0 0 24 24"><polygon points="12 2 2 7 12 12 22 7"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></div>
|
|
202
|
+
<h5>Registration order</h5>
|
|
203
|
+
<p>Middleware runs in the order registered—add parsers before handlers.</p>
|
|
203
204
|
</div>
|
|
204
|
-
<div class="
|
|
205
|
-
|
|
206
|
-
<div class="
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<code>req.body.files</code> and <code>req.body.fields</code>.
|
|
210
|
-
</p>
|
|
211
|
-
</div>
|
|
205
|
+
<div class="pipe-arrow" aria-hidden="true" style="--i:0"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></div>
|
|
206
|
+
<div class="pipe-stage">
|
|
207
|
+
<div class="pipe-icon"><svg viewBox="0 0 24 24"><path d="M9 3H7a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2 2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h2"/><path d="M15 3h2a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2 2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2"/></svg></div>
|
|
208
|
+
<h5>Body parsing</h5>
|
|
209
|
+
<p>Populates <code>req.body</code>; multipart exposes <code>req.body.files</code> and <code>req.body.fields</code>.</p>
|
|
212
210
|
</div>
|
|
213
|
-
<div class="
|
|
214
|
-
|
|
215
|
-
<div class="
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<code>res.json()</code> or <code>res.send()</code> to respond.
|
|
219
|
-
</p>
|
|
220
|
-
</div>
|
|
211
|
+
<div class="pipe-arrow" aria-hidden="true" style="--i:1"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></div>
|
|
212
|
+
<div class="pipe-stage">
|
|
213
|
+
<div class="pipe-icon"><svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg></div>
|
|
214
|
+
<h5>Handler model</h5>
|
|
215
|
+
<p>Handlers receive <code>req</code> + <code>res</code>. Respond with <code>res.json()</code> or <code>res.send()</code>.</p>
|
|
221
216
|
</div>
|
|
222
|
-
<div class="
|
|
223
|
-
|
|
224
|
-
<div class="
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
options.</p>
|
|
228
|
-
</div>
|
|
217
|
+
<div class="pipe-arrow" aria-hidden="true" style="--i:2"><svg viewBox="0 0 24 24"><polyline points="9 6 15 12 9 18"/></svg></div>
|
|
218
|
+
<div class="pipe-stage">
|
|
219
|
+
<div class="pipe-icon"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div>
|
|
220
|
+
<h5>Static middleware</h5>
|
|
221
|
+
<p>Serves files with Content-Type from extension and supports caching options.</p>
|
|
229
222
|
</div>
|
|
230
223
|
</div>
|
|
231
224
|
</div>
|
|
@@ -36,6 +36,25 @@ function populateTocSub(href, items)
|
|
|
36
36
|
const existing = parentLi.querySelector('ul.toc-sub');
|
|
37
37
|
if (existing) existing.remove();
|
|
38
38
|
|
|
39
|
+
/* Ensure parent has collapsible behaviour */
|
|
40
|
+
if (!parentLi.classList.contains('toc-collapsible'))
|
|
41
|
+
{
|
|
42
|
+
parentLi.classList.add('toc-collapsible', 'toc-collapsed');
|
|
43
|
+
parentLi.style.paddingLeft = '20px';
|
|
44
|
+
|
|
45
|
+
const toggle = document.createElement('button');
|
|
46
|
+
toggle.className = 'toc-collapse-btn';
|
|
47
|
+
toggle.setAttribute('aria-label', 'Toggle section');
|
|
48
|
+
toggle.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
49
|
+
toggle.addEventListener('click', (e) =>
|
|
50
|
+
{
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
parentLi.classList.toggle('toc-collapsed');
|
|
54
|
+
});
|
|
55
|
+
parentLi.insertBefore(toggle, parentLi.firstChild);
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
const sub = document.createElement('ul');
|
|
40
59
|
sub.className = 'toc-sub';
|
|
41
60
|
|
|
@@ -113,7 +113,10 @@ function initWsChat()
|
|
|
113
113
|
{
|
|
114
114
|
const name = encodeURIComponent(nameInput.value || 'anon');
|
|
115
115
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
116
|
-
|
|
116
|
+
// Use the direct HTTPS host+port when behind a reverse proxy that
|
|
117
|
+
// doesn't forward WebSocket upgrades.
|
|
118
|
+
const wsHost = location.port ? location.host : (location.hostname + ':7273');
|
|
119
|
+
ws = new WebSocket(`${proto}//${wsHost}/ws/chat?name=${name}`);
|
|
117
120
|
|
|
118
121
|
ws.onopen = () =>
|
|
119
122
|
{
|
|
@@ -12,6 +12,8 @@ document.addEventListener('DOMContentLoaded', () =>
|
|
|
12
12
|
initTocSidebar();
|
|
13
13
|
initTocNavigation();
|
|
14
14
|
initTocToolbar();
|
|
15
|
+
initTocCollapsible();
|
|
16
|
+
initTocSearch();
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
/* -- Feature Tabs ------------------------------------------------------------ */
|
|
@@ -192,15 +194,21 @@ function initTocToolbar()
|
|
|
192
194
|
});
|
|
193
195
|
});
|
|
194
196
|
|
|
195
|
-
/* -- Expand / Collapse
|
|
197
|
+
/* -- Expand / Collapse sidebar categories only ---------------------- */
|
|
196
198
|
if (toggleBtn)
|
|
197
199
|
{
|
|
198
|
-
let expanded =
|
|
200
|
+
let expanded = true; /* Start expanded */
|
|
201
|
+
toggleBtn.classList.add('acc-expanded');
|
|
199
202
|
|
|
200
203
|
toggleBtn.addEventListener('click', () =>
|
|
201
204
|
{
|
|
202
205
|
expanded = !expanded;
|
|
203
|
-
|
|
206
|
+
|
|
207
|
+
/* Toggle only collapsible TOC categories in the sidebar */
|
|
208
|
+
document.querySelectorAll('.toc-collapsible').forEach(li =>
|
|
209
|
+
{
|
|
210
|
+
li.classList.toggle('toc-collapsed', !expanded);
|
|
211
|
+
});
|
|
204
212
|
|
|
205
213
|
toggleBtn.classList.toggle('acc-expanded', expanded);
|
|
206
214
|
toggleBtn.title = expanded ? 'Collapse all' : 'Expand all';
|
|
@@ -208,3 +216,127 @@ function initTocToolbar()
|
|
|
208
216
|
});
|
|
209
217
|
}
|
|
210
218
|
}
|
|
219
|
+
|
|
220
|
+
/* -- TOC Collapsible Categories ---------------------------------------------- */
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Make sidebar categories that have (or will have) nested sub-items
|
|
224
|
+
* collapsible via a toggle chevron. Clicking the chevron expands/collapses
|
|
225
|
+
* the sub-list. Clicking the link itself still navigates.
|
|
226
|
+
*/
|
|
227
|
+
function initTocCollapsible()
|
|
228
|
+
{
|
|
229
|
+
const items = document.querySelectorAll('.toc-collapsible');
|
|
230
|
+
items.forEach(li =>
|
|
231
|
+
{
|
|
232
|
+
/* Start expanded */
|
|
233
|
+
|
|
234
|
+
/* Create toggle button */
|
|
235
|
+
const toggle = document.createElement('button');
|
|
236
|
+
toggle.className = 'toc-collapse-btn';
|
|
237
|
+
toggle.setAttribute('aria-label', 'Toggle section');
|
|
238
|
+
toggle.innerHTML = '<svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 1l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
239
|
+
|
|
240
|
+
toggle.addEventListener('click', (e) =>
|
|
241
|
+
{
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
e.stopPropagation();
|
|
244
|
+
li.classList.toggle('toc-collapsed');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
li.insertBefore(toggle, li.firstChild);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* -- TOC Search Filter ------------------------------------------------------- */
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Wire the sidebar search input to filter TOC items by matching against
|
|
255
|
+
* the display name (link text) of each item in the sidebar.
|
|
256
|
+
*/
|
|
257
|
+
function initTocSearch()
|
|
258
|
+
{
|
|
259
|
+
const input = document.getElementById('toc-search');
|
|
260
|
+
if (!input) return;
|
|
261
|
+
|
|
262
|
+
const nav = document.querySelector('.toc-sidebar nav ul');
|
|
263
|
+
if (!nav) return;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get the visible display name for a TOC list item.
|
|
267
|
+
*/
|
|
268
|
+
function getDisplayName(li)
|
|
269
|
+
{
|
|
270
|
+
const a = li.querySelector(':scope > a');
|
|
271
|
+
return a ? a.textContent.trim().toLowerCase() : '';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check whether any sub-items match the query by display name.
|
|
276
|
+
*/
|
|
277
|
+
function hasSubMatch(li, q)
|
|
278
|
+
{
|
|
279
|
+
const subItems = li.querySelectorAll('.toc-sub-item');
|
|
280
|
+
for (const sub of subItems)
|
|
281
|
+
{
|
|
282
|
+
if (getDisplayName(sub).includes(q)) return true;
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let debounceTimer = null;
|
|
288
|
+
|
|
289
|
+
input.addEventListener('input', () =>
|
|
290
|
+
{
|
|
291
|
+
clearTimeout(debounceTimer);
|
|
292
|
+
debounceTimer = setTimeout(() =>
|
|
293
|
+
{
|
|
294
|
+
const q = input.value.trim().toLowerCase();
|
|
295
|
+
|
|
296
|
+
const topItems = nav.querySelectorAll(':scope > li');
|
|
297
|
+
|
|
298
|
+
if (!q)
|
|
299
|
+
{
|
|
300
|
+
/* Reset: show everything, restore collapsed state */
|
|
301
|
+
topItems.forEach(li =>
|
|
302
|
+
{
|
|
303
|
+
li.style.display = '';
|
|
304
|
+
const subItems = li.querySelectorAll('.toc-sub-item');
|
|
305
|
+
subItems.forEach(s => s.style.display = '');
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
topItems.forEach(li =>
|
|
311
|
+
{
|
|
312
|
+
const titleMatch = getDisplayName(li).includes(q);
|
|
313
|
+
const subMatch = hasSubMatch(li, q);
|
|
314
|
+
|
|
315
|
+
if (titleMatch || subMatch)
|
|
316
|
+
{
|
|
317
|
+
li.style.display = '';
|
|
318
|
+
/* Auto-expand when searching */
|
|
319
|
+
if (li.classList.contains('toc-collapsible'))
|
|
320
|
+
{
|
|
321
|
+
li.classList.remove('toc-collapsed');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* Filter sub-items if only some match */
|
|
325
|
+
const subItems = li.querySelectorAll('.toc-sub-item');
|
|
326
|
+
if (subItems.length)
|
|
327
|
+
{
|
|
328
|
+
subItems.forEach(sub =>
|
|
329
|
+
{
|
|
330
|
+
/* If parent title matched, show all children; otherwise filter by sub-item name */
|
|
331
|
+
sub.style.display = (titleMatch || getDisplayName(sub).includes(q)) ? '' : 'none';
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else
|
|
336
|
+
{
|
|
337
|
+
li.style.display = 'none';
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}, 120);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -133,28 +133,43 @@ function addTrashRow(name)
|
|
|
133
133
|
*/
|
|
134
134
|
function showUndo(name)
|
|
135
135
|
{
|
|
136
|
+
/* Remove any existing undo toast first */
|
|
137
|
+
const prev = document.querySelector('.undo-toast');
|
|
138
|
+
if (prev) prev.remove();
|
|
139
|
+
|
|
136
140
|
const box = document.createElement('div');
|
|
137
|
-
box.className = '
|
|
138
|
-
box.textContent = `Trashed ${name}
|
|
141
|
+
box.className = 'undo-toast';
|
|
142
|
+
box.textContent = `Trashed ${name} \u2014 `;
|
|
139
143
|
|
|
140
144
|
const btn = document.createElement('button');
|
|
141
145
|
btn.textContent = 'Undo';
|
|
142
146
|
btn.className = 'btn';
|
|
143
147
|
box.appendChild(btn);
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
const close = document.createElement('button');
|
|
150
|
+
close.className = 'undo-toast-close';
|
|
151
|
+
close.innerHTML = '×';
|
|
152
|
+
close.setAttribute('aria-label', 'Dismiss');
|
|
153
|
+
box.appendChild(close);
|
|
154
|
+
|
|
155
|
+
document.body.appendChild(box);
|
|
156
|
+
|
|
157
|
+
const dismiss = () => { clearTimeout(tid); box.remove(); document.removeEventListener('click', outsideClick); };
|
|
158
|
+
|
|
159
|
+
const tid = setTimeout(dismiss, 8000);
|
|
160
|
+
|
|
161
|
+
close.addEventListener('click', (e) => { e.stopPropagation(); dismiss(); });
|
|
150
162
|
|
|
151
|
-
|
|
163
|
+
/* Click outside to dismiss */
|
|
164
|
+
function outsideClick(e) { if (!box.contains(e.target)) dismiss(); }
|
|
165
|
+
/* Delay listener so the current click doesn't immediately close it */
|
|
166
|
+
setTimeout(() => document.addEventListener('click', outsideClick), 0);
|
|
152
167
|
|
|
153
|
-
btn.addEventListener('click', async () =>
|
|
168
|
+
btn.addEventListener('click', async (e) =>
|
|
154
169
|
{
|
|
155
|
-
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
dismiss();
|
|
156
172
|
await fetch('/uploads/' + encodeURIComponent(name) + '/restore', { method: 'POST' });
|
|
157
|
-
box.remove();
|
|
158
173
|
try { await loadUploadsCombined(); } catch (e) { }
|
|
159
174
|
loadUploadsList();
|
|
160
175
|
loadTrashList();
|
|
@@ -198,13 +198,18 @@ details.acc[open] > summary:before{transform:rotate(90deg)}
|
|
|
198
198
|
.feature-card h5{margin:0;font-size:13px;line-height:1.2}
|
|
199
199
|
.feature-card p{margin:0;color:var(--muted);font-size:12px;line-height:1.3}
|
|
200
200
|
.feature-text{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
201
|
-
.
|
|
202
|
-
.
|
|
203
|
-
.
|
|
204
|
-
.
|
|
205
|
-
.
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
.pipeline{display:grid;grid-template-columns:1fr auto 1fr auto 1fr auto 1fr;align-items:start;gap:6px}
|
|
202
|
+
.pipe-stage{text-align:center;padding:16px 10px;border-radius:12px;background:linear-gradient(180deg,rgba(255,255,255,0.025),transparent);border:1px solid rgba(255,255,255,0.04);transition:transform .12s ease,box-shadow .12s ease}
|
|
203
|
+
.pipe-stage:hover{transform:translateY(-3px);box-shadow:0 12px 28px rgba(0,0,0,0.5)}
|
|
204
|
+
.pipe-icon{width:46px;height:46px;border-radius:13px;background:linear-gradient(135deg,var(--accent),var(--accent-2));display:flex;align-items:center;justify-content:center;margin:0 auto 10px;box-shadow:0 6px 16px rgba(88,101,242,0.15)}
|
|
205
|
+
.pipe-icon svg{width:22px;height:22px;fill:none;stroke:#fff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
206
|
+
.pipe-stage h5{margin:0 0 5px;font-size:13px}
|
|
207
|
+
.pipe-stage p{margin:0;color:var(--muted);font-size:12px;line-height:1.45}
|
|
208
|
+
.pipe-arrow{display:flex;align-items:center;padding-top:18px}
|
|
209
|
+
@keyframes flowPulse{0%,100%{opacity:0.25}50%{opacity:0.55}}
|
|
210
|
+
.pipe-arrow svg{width:22px;height:22px;fill:none;stroke:var(--accent);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;animation:flowPulse 2.5s ease-in-out infinite;animation-delay:calc(var(--i,0) * 0.5s)}
|
|
211
|
+
|
|
212
|
+
@media(max-width:900px){.feature-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}.tabs-nav{flex-wrap:wrap}.pipeline{grid-template-columns:1fr;justify-items:center;gap:2px}.pipe-stage{max-width:380px;width:100%}.pipe-arrow{padding:2px 0}.pipe-arrow svg{transform:rotate(90deg)}}
|
|
208
213
|
@media(max-width:500px){.feature-grid{grid-template-columns:1fr}}
|
|
209
214
|
|
|
210
215
|
@media(max-width:900px){.playgrid{grid-template-columns:1fr}.ui-shell{padding:16px}}
|
|
@@ -270,8 +275,8 @@ details.acc[open] > summary:before{transform:rotate(90deg)}
|
|
|
270
275
|
body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto}
|
|
271
276
|
|
|
272
277
|
/* TOC icon toolbar */
|
|
273
|
-
.toc-toolbar{display:flex;gap:6px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,0.04)}
|
|
274
|
-
.toc-tool-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--muted);cursor:pointer;transition:color .12s ease,background .12s ease,transform .12s ease}
|
|
278
|
+
.toc-toolbar{display:flex;align-items:center;gap:6px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,0.04)}
|
|
279
|
+
.toc-tool-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex-shrink:0;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--muted);cursor:pointer;transition:color .12s ease,background .12s ease,transform .12s ease}
|
|
275
280
|
.toc-tool-btn:hover{color:var(--accent);background:rgba(88,101,242,0.08)}
|
|
276
281
|
.toc-tool-btn:active{transform:scale(0.92)}
|
|
277
282
|
.toc-tool-btn.acc-expanded svg{transform:rotate(180deg);transition:transform .18s ease}
|
|
@@ -284,6 +289,21 @@ body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto
|
|
|
284
289
|
.toc-sidebar nav li.toc-sub-item a:hover{color:var(--text);background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent)}
|
|
285
290
|
.toc-sidebar nav li.toc-sub-item a:visited{color:rgba(255,255,255,0.6)}
|
|
286
291
|
|
|
292
|
+
/* TOC search filter (inline in toolbar) */
|
|
293
|
+
.toc-search-wrap{position:relative;flex:1;min-width:0}
|
|
294
|
+
.toc-search-icon{position:absolute;left:8px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none}
|
|
295
|
+
.toc-search{width:100%;box-sizing:border-box;padding:6px 8px 6px 26px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);color:var(--text);font-size:12px;outline:none;transition:border-color .15s ease,box-shadow .15s ease;height:32px}
|
|
296
|
+
.toc-search::placeholder{color:rgba(255,255,255,0.3)}
|
|
297
|
+
.toc-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(88,101,242,0.12)}
|
|
298
|
+
|
|
299
|
+
/* Collapsible TOC categories */
|
|
300
|
+
.toc-collapsible{position:relative;padding-left:20px !important}
|
|
301
|
+
.toc-collapse-btn{position:absolute;left:2px;top:12px;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--muted);cursor:pointer;padding:0;border-radius:4px;transition:color .12s ease,background .12s ease}
|
|
302
|
+
.toc-collapse-btn:hover{color:var(--accent);background:rgba(88,101,242,0.08)}
|
|
303
|
+
.toc-collapse-btn svg{transition:transform .18s ease}
|
|
304
|
+
.toc-collapsible:not(.toc-collapsed) > .toc-collapse-btn svg{transform:rotate(90deg)}
|
|
305
|
+
.toc-collapsible.toc-collapsed > .toc-sub{display:none !important}
|
|
306
|
+
|
|
287
307
|
|
|
288
308
|
@media(min-width:900px){
|
|
289
309
|
.ui-shell{display:flex;align-items:flex-start}
|
|
@@ -297,7 +317,6 @@ body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto
|
|
|
297
317
|
.ui-main{margin-left:0}
|
|
298
318
|
}
|
|
299
319
|
|
|
300
|
-
/* Keep the hamburger always accessible on small screens */
|
|
301
320
|
@media(max-width:640px){
|
|
302
321
|
:root{--header-height:56px}
|
|
303
322
|
.ui-header{width:calc(100% - 16px);padding:8px 12px;border-radius:0}
|
|
@@ -305,6 +324,10 @@ body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto
|
|
|
305
324
|
.brand .logo{font-size:18px}
|
|
306
325
|
}
|
|
307
326
|
|
|
308
|
-
/* Ensure page content is not hidden behind the fixed header */
|
|
309
327
|
body{padding-top:calc(var(--header-height) + 12px)}
|
|
310
|
-
.ui-shell{margin-top:0}
|
|
328
|
+
.ui-shell{margin-top:0}
|
|
329
|
+
|
|
330
|
+
.undo-toast{position:fixed;top:calc(var(--header-height) + 12px);left:50%;transform:translateX(-50%);z-index:200;background:var(--surface, #161a22);border:1px solid var(--surface-border, rgba(255,255,255,0.06));color:var(--text, #e2e8f0);padding:10px 18px;border-radius:10px;font-size:14px;box-shadow:0 6px 20px rgba(0,0,0,0.5);display:flex;align-items:center;gap:10px;animation:toast-in .25s ease}
|
|
331
|
+
@keyframes toast-in{from{opacity:0;transform:translateX(-50%) translateY(-10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
|
|
332
|
+
.undo-toast-close{background:none;border:none;color:var(--text, #e2e8f0);font-size:18px;line-height:1;cursor:pointer;padding:0 0 0 4px;opacity:.5;transition:opacity .15s}
|
|
333
|
+
.undo-toast-close:hover{opacity:1}
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
<!-- Outer large container (uncompressed) -->
|
|
10
10
|
<rect x="6" y="8" width="24" height="48" rx="5" fill="url(#g-comp)" opacity="0.3"/>
|
|
11
11
|
<!-- Inward arrows implying compression -->
|
|
12
|
-
<path d="M6 20h8" stroke="#
|
|
13
|
-
<path d="M30 20h-8" stroke="#
|
|
14
|
-
<path d="M6 32h8" stroke="#
|
|
15
|
-
<path d="M30 32h-8" stroke="#
|
|
16
|
-
<path d="M6 44h8" stroke="#
|
|
17
|
-
<path d="M30 44h-8" stroke="#
|
|
12
|
+
<path d="M6 20h8" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
|
|
13
|
+
<path d="M30 20h-8" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
|
|
14
|
+
<path d="M6 32h8" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
|
|
15
|
+
<path d="M30 32h-8" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
|
|
16
|
+
<path d="M6 44h8" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
|
|
17
|
+
<path d="M30 44h-8" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
|
|
18
18
|
<!-- Arrow pointing to compressed result -->
|
|
19
19
|
<path d="M34 32h6" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round"/>
|
|
20
20
|
<path d="M38 28l4 4-4 4" fill="none" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
<rect width="64" height="64" rx="8" fill="transparent"/>
|
|
9
9
|
<!-- Shield shape -->
|
|
10
10
|
<path d="M32 6 L52 16 L52 34 C52 48 32 58 32 58 C32 58 12 48 12 34 L12 16 Z" fill="url(#g-tls)"/>
|
|
11
|
-
<!-- White highlight on shield -->
|
|
12
|
-
<path d="M32 10 L48 18 L48 33 C48 44 32 53 32 53 C32 53 30 52 28 50" fill="rgba(255,255,255,0.08)" stroke="none"/>
|
|
13
11
|
<!-- Lock body -->
|
|
14
12
|
<rect x="24" y="30" width="16" height="14" rx="3" fill="rgba(255,255,255,0.9)"/>
|
|
15
13
|
<!-- Lock shackle -->
|
|
@@ -17,7 +15,4 @@
|
|
|
17
15
|
<!-- Keyhole -->
|
|
18
16
|
<circle cx="32" cy="36" r="2.5" fill="url(#g-tls)"/>
|
|
19
17
|
<rect x="31" y="36" width="2" height="4" rx="1" fill="url(#g-tls)"/>
|
|
20
|
-
<!-- Sparkle dots -->
|
|
21
|
-
<circle cx="50" cy="10" r="1.5" fill="rgba(255,255,255,0.6)"/>
|
|
22
|
-
<circle cx="54" cy="14" r="1" fill="rgba(255,255,255,0.35)"/>
|
|
23
18
|
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#00c6ff"></stop>
|
|
5
|
+
<stop offset="100%" stop-color="#0072ff"></stop>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<rect width="512" height="512" rx="90" fill="#0f172a"></rect>
|
|
10
|
+
|
|
11
|
+
<circle cx="256" cy="230" r="110" fill="none" stroke="url(#grad)" stroke-width="28"></circle>
|
|
12
|
+
|
|
13
|
+
<line x1="256" y1="120" x2="256" y2="340" stroke="#0f172a" stroke-width="28"></line>
|
|
14
|
+
|
|
15
|
+
<rect x="180" y="190" width="152" height="80" rx="16" fill="#0f172a"></rect>
|
|
16
|
+
|
|
17
|
+
<text x="256" y="245" text-anchor="middle" font-family="Segoe UI, Arial, sans-serif" font-size="42" font-weight="700" fill="url(#grad)">
|
|
18
|
+
HTTP
|
|
19
|
+
</text>
|
|
20
|
+
|
|
21
|
+
<text x="256" y="430" text-anchor="middle" font-family="Segoe UI, Arial, sans-serif" font-size="44" font-weight="600" fill="white" letter-spacing="4">
|
|
22
|
+
ZERO
|
|
23
|
+
</text>
|
|
24
|
+
</svg>
|
|
@@ -24,6 +24,4 @@
|
|
|
24
24
|
<rect x="22" y="50" width="20" height="10" rx="3.5" fill="url(#g-rtr)" opacity="0.7"/>
|
|
25
25
|
<line x1="26" y1="55" x2="38" y2="55" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
|
|
26
26
|
<line x1="26" y1="58" x2="34" y2="58" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round"/>
|
|
27
|
-
<!-- Sparkle -->
|
|
28
|
-
<circle cx="56" cy="28" r="1.5" fill="rgba(255,255,255,0.5)"/>
|
|
29
27
|
</svg>
|
|
@@ -19,7 +19,4 @@
|
|
|
19
19
|
<path d="M49 28l5 4-5 4" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.7"/>
|
|
20
20
|
<line x1="36" y1="40" x2="54" y2="40" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round" opacity="0.45"/>
|
|
21
21
|
<path d="M51 36l5 4-5 4" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.45"/>
|
|
22
|
-
<!-- White sparkle -->
|
|
23
|
-
<circle cx="54" cy="18" r="2" fill="rgba(255,255,255,0.7)"/>
|
|
24
|
-
<circle cx="57" cy="14" r="1" fill="rgba(255,255,255,0.4)"/>
|
|
25
22
|
</svg>
|
package/lib/app.js
CHANGED
|
@@ -247,7 +247,18 @@ class App
|
|
|
247
247
|
*/
|
|
248
248
|
routes()
|
|
249
249
|
{
|
|
250
|
-
|
|
250
|
+
const list = this.router.inspect();
|
|
251
|
+
|
|
252
|
+
/* Include WebSocket upgrade handlers */
|
|
253
|
+
for (const [wsPath, { opts }] of this._wsHandlers)
|
|
254
|
+
{
|
|
255
|
+
const entry = { method: 'WS', path: wsPath };
|
|
256
|
+
if (opts && opts.maxPayload !== undefined) entry.maxPayload = opts.maxPayload;
|
|
257
|
+
if (opts && opts.pingInterval !== undefined) entry.pingInterval = opts.pingInterval;
|
|
258
|
+
list.push(entry);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return list;
|
|
251
262
|
}
|
|
252
263
|
|
|
253
264
|
// -- Route Registration ----------------------------
|