zero-http 0.2.1 → 0.2.3

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 CHANGED
@@ -1,4 +1,8 @@
1
- # zero-http
1
+ <p align="center">
2
+ <img src="documentation/public/vendor/icons/logo.svg" alt="zero-http logo" width="300" height="300">
3
+ </p>
4
+
5
+ <h1 align="center">zero-http</h1>
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/zero-http.svg)](https://www.npmjs.com/package/zero-http)
4
8
  [![npm downloads](https://img.shields.io/npm/dm/zero-http.svg)](https://www.npmjs.com/package/zero-http)
@@ -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) => res.json(app.routes()));
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 || 3000;
180
- const server = app.listen(port, () =>
192
+ const port = process.env.PORT || 7273;
193
+ const server = app.listen(port, tlsOpts, () =>
181
194
  {
182
- console.log(`zero-http full-server listening on http://localhost:${port}`);
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 base = `http://localhost:${port}`;
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) =>
@@ -41,9 +41,12 @@
41
41
  </svg>
42
42
  </button>
43
43
 
44
- <a class="brand" href="#top" id="brand-top" style="text-decoration:none;color:inherit;cursor:pointer">
45
- <div class="logo">zero-http</div>
46
- <div class="subtitle">Zero-dependency Express-like server</div>
44
+ <a class="brand" href="#top" id="brand-top" style="text-decoration:none;color:inherit;cursor:pointer;display:flex;align-items:center;gap:12px">
45
+ <img src="/vendor/icons/logo.svg" alt="zero-http logo" class="header-logo" width="50" height="50" />
46
+ <div>
47
+ <div class="logo">zero-http</div>
48
+ <div class="subtitle">Zero-dependency Express-like server</div>
49
+ </div>
47
50
  </a>
48
51
 
49
52
  <div class="repo-buttons" role="navigation" aria-label="Repository links">
@@ -71,20 +74,24 @@
71
74
  <aside class="toc-sidebar" aria-label="Table of contents">
72
75
  <nav>
73
76
  <div class="toc-toolbar">
77
+ <div class="toc-search-wrap">
78
+ <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>
79
+ <input id="toc-search" class="toc-search" type="text" placeholder="Filter…" autocomplete="off" spellcheck="false" />
80
+ </div>
81
+ <button class="toc-tool-btn" id="toc-toggle-acc" title="Collapse all" aria-label="Collapse all sections">
82
+ <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>
83
+ </button>
74
84
  <button class="toc-tool-btn" id="toc-top-btn" title="Back to top" aria-label="Scroll to top">
75
85
  <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
86
  </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
87
  </div>
81
88
  <ul>
82
89
  <li><a href="#features">Features</a></li>
83
90
  <li><a href="#quickstart">Quickstart</a></li>
84
91
  <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>
92
+ <li class="toc-collapsible"><a href="#api-reference">API Reference</a></li>
93
+ <li class="toc-collapsible"><a href="#simple-examples">Simple Examples</a></li>
94
+ <li class="toc-collapsible"><a href="#playground">Playground</a>
88
95
  <ul class="toc-sub">
89
96
  <li class="toc-sub-item"><a href="#upload-testing">Upload multipart data</a></li>
90
97
  <li class="toc-sub-item"><a href="#parser-tests">Parser tests</a></li>
@@ -192,40 +199,29 @@
192
199
  </div>
193
200
  </div>
194
201
  <div id="tab-behavior" class="tab-panel">
195
- <div class="behavior-list">
196
- <div class="behavior-step">
197
- <div class="step-num">1</div>
198
- <div class="step-body">
199
- <h5>Registration order</h5>
200
- <p>Routing and middleware run in the order they're registered—add parsers before
201
- handlers.</p>
202
- </div>
202
+ <div class="pipeline">
203
+ <div class="pipe-stage">
204
+ <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>
205
+ <h5>Registration order</h5>
206
+ <p>Middleware runs in the order registered—add parsers before handlers.</p>
203
207
  </div>
204
- <div class="behavior-step">
205
- <div class="step-num">2</div>
206
- <div class="step-body">
207
- <h5>Body parsing</h5>
208
- <p>Body parsers populate <code>req.body</code>; multipart uploads expose
209
- <code>req.body.files</code> and <code>req.body.fields</code>.
210
- </p>
211
- </div>
208
+ <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>
209
+ <div class="pipe-stage">
210
+ <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>
211
+ <h5>Body parsing</h5>
212
+ <p>Populates <code>req.body</code>; multipart exposes <code>req.body.files</code> and <code>req.body.fields</code>.</p>
212
213
  </div>
213
- <div class="behavior-step">
214
- <div class="step-num">3</div>
215
- <div class="step-body">
216
- <h5>Handler model</h5>
217
- <p>Handlers receive <code>req</code> and <code>res</code>. Use
218
- <code>res.json()</code> or <code>res.send()</code> to respond.
219
- </p>
220
- </div>
214
+ <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>
215
+ <div class="pipe-stage">
216
+ <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>
217
+ <h5>Handler model</h5>
218
+ <p>Handlers receive <code>req</code> + <code>res</code>. Respond with <code>res.json()</code> or <code>res.send()</code>.</p>
221
219
  </div>
222
- <div class="behavior-step">
223
- <div class="step-num">4</div>
224
- <div class="step-body">
225
- <h5>Static middleware</h5>
226
- <p>Serves files with Content-Type derived from extension and supports caching
227
- options.</p>
228
- </div>
220
+ <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>
221
+ <div class="pipe-stage">
222
+ <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>
223
+ <h5>Static middleware</h5>
224
+ <p>Serves files with Content-Type from extension and supports caching options.</p>
229
225
  </div>
230
226
  </div>
231
227
  </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
- ws = new WebSocket(`${proto}//${location.host}/ws/chat?name=${name}`);
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 all accordions ------------------------------- */
197
+ /* -- Expand / Collapse sidebar categories only ---------------------- */
196
198
  if (toggleBtn)
197
199
  {
198
- let expanded = false;
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
- document.querySelectorAll('details.acc').forEach(d => d.open = expanded);
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 = 'panel';
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
- try
146
- {
147
- const shell = document.querySelector('.ui-shell') || document.body;
148
- shell.prepend(box);
149
- } catch (e) { }
149
+ const close = document.createElement('button');
150
+ close.className = 'undo-toast-close';
151
+ close.innerHTML = '&times;';
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
- const tid = setTimeout(() => box.remove(), 8000);
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
- clearTimeout(tid);
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();
@@ -26,6 +26,7 @@ html{font-size:18px}
26
26
  body{height:100%;margin:0;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;line-height:1.6;font-family:'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif}
27
27
  .ui-shell{max-width:1300px;margin:24px auto;padding:28px}
28
28
  .ui-header{display:flex;align-items:center;gap:16px;margin-bottom:14px;position:fixed;top:0;left:50%;transform:translateX(-50%);width:calc(100% - 48px);max-width:1300px;height:var(--header-height);z-index:140;background:linear-gradient(180deg, rgba(11,13,16,0.96), rgba(11,13,16,0.92));backdrop-filter:blur(6px);padding:10px 20px;border-radius:10px}
29
+ .header-logo{width:50px;height:auto;flex-shrink:0}
29
30
  .repo-buttons{margin-left:auto;display:flex;gap:12px;align-items:center}
30
31
  .icon-btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:10px 12px;border-radius:12px;color:var(--text);background:transparent;border:1px solid rgba(255,255,255,0.03);cursor:pointer;text-decoration:none;font-weight:700;transition:transform .08s ease,box-shadow .12s ease,background .12s ease;min-width:44px;min-height:44px}
31
32
  .icon-btn svg, .icon-btn .icon-svg{display:block;width:28px;height:28px}
@@ -198,13 +199,18 @@ details.acc[open] > summary:before{transform:rotate(90deg)}
198
199
  .feature-card h5{margin:0;font-size:13px;line-height:1.2}
199
200
  .feature-card p{margin:0;color:var(--muted);font-size:12px;line-height:1.3}
200
201
  .feature-text{display:flex;flex-direction:column;gap:2px;min-width:0}
201
- .behavior-list{display:flex;flex-direction:column;gap:10px}
202
- .behavior-step{display:flex;gap:12px;align-items:flex-start;padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.03);background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent)}
203
- .step-num{width:44px;height:44px;border-radius:10px;background:var(--accent);display:flex;align-items:center;justify-content:center;font-weight:800;color:#fff;box-shadow:0 8px 18px rgba(31,142,253,0.08)}
204
- .step-body h5{margin:0 0 6px 0}
205
- .step-body p{margin:0;color:var(--muted)}
206
-
207
- @media(max-width:900px){.feature-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}.tabs-nav{flex-wrap:wrap}}
202
+ .pipeline{display:grid;grid-template-columns:1fr auto 1fr auto 1fr auto 1fr;align-items:start;gap:6px}
203
+ .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}
204
+ .pipe-stage:hover{transform:translateY(-3px);box-shadow:0 12px 28px rgba(0,0,0,0.5)}
205
+ .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)}
206
+ .pipe-icon svg{width:22px;height:22px;fill:none;stroke:#fff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
207
+ .pipe-stage h5{margin:0 0 5px;font-size:13px}
208
+ .pipe-stage p{margin:0;color:var(--muted);font-size:12px;line-height:1.45}
209
+ .pipe-arrow{display:flex;align-items:center;padding-top:18px}
210
+ @keyframes flowPulse{0%,100%{opacity:0.25}50%{opacity:0.55}}
211
+ .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)}
212
+
213
+ @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
214
  @media(max-width:500px){.feature-grid{grid-template-columns:1fr}}
209
215
 
210
216
  @media(max-width:900px){.playgrid{grid-template-columns:1fr}.ui-shell{padding:16px}}
@@ -270,8 +276,8 @@ details.acc[open] > summary:before{transform:rotate(90deg)}
270
276
  body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto}
271
277
 
272
278
  /* 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}
279
+ .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)}
280
+ .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
281
  .toc-tool-btn:hover{color:var(--accent);background:rgba(88,101,242,0.08)}
276
282
  .toc-tool-btn:active{transform:scale(0.92)}
277
283
  .toc-tool-btn.acc-expanded svg{transform:rotate(180deg);transition:transform .18s ease}
@@ -284,6 +290,21 @@ body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto
284
290
  .toc-sidebar nav li.toc-sub-item a:hover{color:var(--text);background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent)}
285
291
  .toc-sidebar nav li.toc-sub-item a:visited{color:rgba(255,255,255,0.6)}
286
292
 
293
+ /* TOC search filter (inline in toolbar) */
294
+ .toc-search-wrap{position:relative;flex:1;min-width:0}
295
+ .toc-search-icon{position:absolute;left:8px;top:50%;transform:translateY(-50%);color:var(--muted);pointer-events:none}
296
+ .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}
297
+ .toc-search::placeholder{color:rgba(255,255,255,0.3)}
298
+ .toc-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(88,101,242,0.12)}
299
+
300
+ /* Collapsible TOC categories */
301
+ .toc-collapsible{position:relative;padding-left:20px !important}
302
+ .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}
303
+ .toc-collapse-btn:hover{color:var(--accent);background:rgba(88,101,242,0.08)}
304
+ .toc-collapse-btn svg{transition:transform .18s ease}
305
+ .toc-collapsible:not(.toc-collapsed) > .toc-collapse-btn svg{transform:rotate(90deg)}
306
+ .toc-collapsible.toc-collapsed > .toc-sub{display:none !important}
307
+
287
308
 
288
309
  @media(min-width:900px){
289
310
  .ui-shell{display:flex;align-items:flex-start}
@@ -297,7 +318,6 @@ body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto
297
318
  .ui-main{margin-left:0}
298
319
  }
299
320
 
300
- /* Keep the hamburger always accessible on small screens */
301
321
  @media(max-width:640px){
302
322
  :root{--header-height:56px}
303
323
  .ui-header{width:calc(100% - 16px);padding:8px 12px;border-radius:0}
@@ -305,6 +325,10 @@ body.toc-open .toc-sidebar{opacity:1;transform:translateX(0);pointer-events:auto
305
325
  .brand .logo{font-size:18px}
306
326
  }
307
327
 
308
- /* Ensure page content is not hidden behind the fixed header */
309
328
  body{padding-top:calc(var(--header-height) + 12px)}
310
- .ui-shell{margin-top:0}
329
+ .ui-shell{margin-top:0}
330
+
331
+ .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}
332
+ @keyframes toast-in{from{opacity:0;transform:translateX(-50%) translateY(-10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
333
+ .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}
334
+ .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="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
13
- <path d="M30 20h-8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
14
- <path d="M6 32h8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
15
- <path d="M30 32h-8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
16
- <path d="M6 44h8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
17
- <path d="M30 44h-8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
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,89 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="60 40 392 432" width="512" height="512" aria-label="zero-http logo">
2
+ <defs>
3
+ <!-- Primary purple-to-cyan gradient -->
4
+ <linearGradient id="logo-grad" x1="0" y1="0" x2="1" y2="1">
5
+ <stop offset="0%" stop-color="#7b61ff"/>
6
+ <stop offset="50%" stop-color="#5865f2"/>
7
+ <stop offset="100%" stop-color="#3ec6ff"/>
8
+ </linearGradient>
9
+ <!-- Lighter glow version -->
10
+ <linearGradient id="logo-glow" x1="0" y1="0" x2="1" y2="1">
11
+ <stop offset="0%" stop-color="#a78bfa"/>
12
+ <stop offset="100%" stop-color="#67e8f9"/>
13
+ </linearGradient>
14
+ <!-- Subtle inner highlight -->
15
+ <linearGradient id="logo-hi" x1="0.5" y1="0" x2="0.5" y2="1">
16
+ <stop offset="0%" stop-color="rgba(255,255,255,0.25)"/>
17
+ <stop offset="100%" stop-color="rgba(255,255,255,0)"/>
18
+ </linearGradient>
19
+ <!-- Background radial glow -->
20
+ <radialGradient id="bg-glow" cx="50%" cy="50%" r="50%">
21
+ <stop offset="0%" stop-color="#5865f2" stop-opacity="0.12"/>
22
+ <stop offset="100%" stop-color="#0e1114" stop-opacity="0"/>
23
+ </radialGradient>
24
+ <!-- Drop shadow filter -->
25
+ <filter id="logo-shadow" x="-20%" y="-20%" width="140%" height="140%">
26
+ <feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#5865f2" flood-opacity="0.35"/>
27
+ </filter>
28
+ <!-- Soft glow filter -->
29
+ <filter id="bolt-glow" x="-30%" y="-30%" width="160%" height="160%">
30
+ <feGaussianBlur stdDeviation="4" result="blur"/>
31
+ <feComposite in="SourceGraphic" in2="blur" operator="over"/>
32
+ </filter>
33
+ </defs>
34
+
35
+ <!-- Background glow -->
36
+ <circle cx="256" cy="256" r="200" fill="url(#bg-glow)"/>
37
+
38
+ <!-- Outer ring — perfect circle -->
39
+ <circle cx="256" cy="256" r="195" fill="none"
40
+ stroke="url(#logo-grad)" stroke-width="24"
41
+ stroke-linecap="round" opacity="0.15"/>
42
+
43
+ <!-- Main ring — perfect circle -->
44
+ <circle cx="256" cy="256" r="170" fill="none"
45
+ stroke="url(#logo-grad)" stroke-width="32"
46
+ stroke-linecap="round" filter="url(#logo-shadow)"/>
47
+
48
+ <!-- Highlight arc on top-left of the ring -->
49
+ <path d="M 140,175 A 170,170 0 0,1 330,100"
50
+ fill="none" stroke="url(#logo-hi)" stroke-width="14"
51
+ stroke-linecap="round" opacity="0.6"/>
52
+
53
+ <!-- Lightning bolt — centered and thicker -->
54
+ <g filter="url(#bolt-glow)">
55
+ <!-- Bolt body -->
56
+ <path d="
57
+ M 222 82
58
+ L 186 242
59
+ L 252 242
60
+ L 182 435
61
+ L 330 205
62
+ L 258 205
63
+ L 296 82
64
+ Z
65
+ " fill="url(#logo-grad)"/>
66
+
67
+ <!-- Bolt inner highlight -->
68
+ <path d="
69
+ M 228 100
70
+ L 202 238
71
+ L 252 238
72
+ L 204 395
73
+ L 318 212
74
+ L 262 212
75
+ L 288 100
76
+ Z
77
+ " fill="url(#logo-hi)" opacity="0.5"/>
78
+ </g>
79
+
80
+ <!-- Small orbital dots — representing HTTP connections -->
81
+ <circle cx="100" cy="155" r="8" fill="#7b61ff" opacity="0.7"/>
82
+ <circle cx="410" cy="360" r="8" fill="#3ec6ff" opacity="0.7"/>
83
+ <circle cx="88" cy="330" r="5" fill="#5865f2" opacity="0.5"/>
84
+ <circle cx="425" cy="170" r="5" fill="#67e8f9" opacity="0.5"/>
85
+
86
+ <!-- Subtle angled scan lines for tech feel -->
87
+ <line x1="120" y1="400" x2="160" y2="415" stroke="#3ec6ff" stroke-width="2" opacity="0.15" stroke-linecap="round"/>
88
+ <line x1="350" y1="95" x2="390" y2="110" stroke="#7b61ff" stroke-width="2" opacity="0.15" stroke-linecap="round"/>
89
+ </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
- return this.router.inspect();
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 ----------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-http",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Zero-dependency, minimal Express-like HTTP server and tiny fetch replacement",
5
5
  "main": "index.js",
6
6
  "files": [