zero-http 0.2.0 → 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.
Files changed (43) hide show
  1. package/README.md +314 -115
  2. package/documentation/full-server.js +102 -5
  3. package/documentation/public/data/api.json +154 -33
  4. package/documentation/public/data/examples.json +35 -11
  5. package/documentation/public/data/options.json +14 -8
  6. package/documentation/public/index.html +138 -53
  7. package/documentation/public/scripts/data-sections.js +23 -4
  8. package/documentation/public/scripts/helpers.js +4 -4
  9. package/documentation/public/scripts/playground.js +204 -0
  10. package/documentation/public/scripts/ui.js +140 -8
  11. package/documentation/public/scripts/uploads.js +35 -20
  12. package/documentation/public/styles.css +46 -23
  13. package/documentation/public/vendor/icons/compress.svg +27 -0
  14. package/documentation/public/vendor/icons/https.svg +18 -0
  15. package/documentation/public/vendor/icons/logo.svg +24 -0
  16. package/documentation/public/vendor/icons/router.svg +27 -0
  17. package/documentation/public/vendor/icons/sse.svg +22 -0
  18. package/documentation/public/vendor/icons/websocket.svg +21 -0
  19. package/index.js +21 -4
  20. package/lib/app.js +156 -15
  21. package/lib/body/json.js +3 -0
  22. package/lib/body/multipart.js +2 -0
  23. package/lib/body/raw.js +3 -0
  24. package/lib/body/text.js +3 -0
  25. package/lib/body/urlencoded.js +3 -0
  26. package/lib/{fetch.js → fetch/index.js} +30 -1
  27. package/lib/http/index.js +9 -0
  28. package/lib/{request.js → http/request.js} +7 -1
  29. package/lib/{response.js → http/response.js} +70 -1
  30. package/lib/middleware/compress.js +194 -0
  31. package/lib/middleware/index.js +12 -0
  32. package/lib/router/index.js +278 -0
  33. package/lib/sse/index.js +8 -0
  34. package/lib/sse/stream.js +322 -0
  35. package/lib/ws/connection.js +440 -0
  36. package/lib/ws/handshake.js +122 -0
  37. package/lib/ws/index.js +12 -0
  38. package/package.json +1 -1
  39. package/lib/router.js +0 -87
  40. /package/lib/{cors.js → middleware/cors.js} +0 -0
  41. /package/lib/{logger.js → middleware/logger.js} +0 -0
  42. /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
  43. /package/lib/{static.js → middleware/static.js} +0 -0
@@ -12,9 +12,11 @@ document.addEventListener('DOMContentLoaded', () =>
12
12
  initTocSidebar();
13
13
  initTocNavigation();
14
14
  initTocToolbar();
15
+ initTocCollapsible();
16
+ initTocSearch();
15
17
  });
16
18
 
17
- /* ── Feature Tabs ──────────────────────────────────────────────────────────── */
19
+ /* -- Feature Tabs ------------------------------------------------------------ */
18
20
 
19
21
  /**
20
22
  * Wire the feature / server-model tab buttons so clicking one activates
@@ -47,7 +49,7 @@ function initFeatureTabs()
47
49
  });
48
50
  }
49
51
 
50
- /* ── TOC Sidebar Toggle ────────────────────────────────────────────────────── */
52
+ /* -- TOC Sidebar Toggle ------------------------------------------------------ */
51
53
 
52
54
  /**
53
55
  * Wire the hamburger button to toggle the sidebar on both desktop (persistent)
@@ -106,7 +108,7 @@ function initTocSidebar()
106
108
  window.addEventListener('resize', syncAria);
107
109
  }
108
110
 
109
- /* ── TOC Smooth-Scroll Navigation ──────────────────────────────────────────── */
111
+ /* -- TOC Smooth-Scroll Navigation -------------------------------------------- */
110
112
 
111
113
  /**
112
114
  * When clicking a TOC link that points to a `#hash`, auto-open any ancestor
@@ -166,7 +168,7 @@ function initTocNavigation()
166
168
  });
167
169
  }
168
170
 
169
- /* ── TOC Toolbar (scroll-to-top & expand/collapse all) ─────────────────────── */
171
+ /* -- TOC Toolbar (scroll-to-top & expand/collapse all) ----------------------- */
170
172
 
171
173
  /**
172
174
  * Wire the icon-bar buttons at the top of the sidebar:
@@ -179,7 +181,7 @@ function initTocToolbar()
179
181
  const toggleBtn = document.getElementById('toc-toggle-acc');
180
182
  if (!topBtn && !toggleBtn) return;
181
183
 
182
- /* ── Scroll to top ────────────────────────────────────────────────── */
184
+ /* -- Scroll to top -------------------------------------------------- */
183
185
  const brandBtn = document.getElementById('brand-top');
184
186
 
185
187
  [topBtn, brandBtn].forEach(el =>
@@ -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
+ }
@@ -7,14 +7,14 @@
7
7
  * showJsonResult, highlightAllPre)
8
8
  */
9
9
 
10
- /* ── Pagination State ──────────────────────────────────────────────────────── */
10
+ /* -- Pagination State -------------------------------------------------------- */
11
11
 
12
12
  let currentPage = 1;
13
13
  let currentSort = 'mtime';
14
14
  let currentOrder = 'desc';
15
15
  const pageSize = 4;
16
16
 
17
- /* ── Combined Uploads + Trash JSON ─────────────────────────────────────────── */
17
+ /* -- Combined Uploads + Trash JSON ------------------------------------------- */
18
18
 
19
19
  /**
20
20
  * Fetch the combined uploads + trash listing and render it as JSON into the
@@ -30,7 +30,7 @@ async function loadUploadsCombined()
30
30
  } catch (e) { }
31
31
  }
32
32
 
33
- /* ── Convenience: DELETE + Show Result ─────────────────────────────────────── */
33
+ /* -- Convenience: DELETE + Show Result --------------------------------------- */
34
34
 
35
35
  /**
36
36
  * Issue a DELETE request and display the JSON response.
@@ -46,7 +46,7 @@ async function deleteAndShow(path, container)
46
46
  try { await loadUploadsCombined(); } catch (e) { showJsonResult(container, j); }
47
47
  }
48
48
 
49
- /* ── Trash Row Factory ─────────────────────────────────────────────────────── */
49
+ /* -- Trash Row Factory ------------------------------------------------------- */
50
50
 
51
51
  /**
52
52
  * Build a trash-row DOM node with Restore and Delete Permanently buttons.
@@ -90,7 +90,7 @@ function createTrashRow(name)
90
90
  return row;
91
91
  }
92
92
 
93
- /* ── Trash List ────────────────────────────────────────────────────────────── */
93
+ /* -- Trash List -------------------------------------------------------------- */
94
94
 
95
95
  /**
96
96
  * Fetch the trash listing from the server and render rows into `#trashList`.
@@ -125,7 +125,7 @@ function addTrashRow(name)
125
125
  else trashList.appendChild(row);
126
126
  }
127
127
 
128
- /* ── Undo Toast ────────────────────────────────────────────────────────────── */
128
+ /* -- Undo Toast -------------------------------------------------------------- */
129
129
 
130
130
  /**
131
131
  * Display a transient undo panel when a file is moved to trash.
@@ -133,35 +133,50 @@ 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();
161
176
  });
162
177
  }
163
178
 
164
- /* ── Upload Card Factory ───────────────────────────────────────────────────── */
179
+ /* -- Upload Card Factory ----------------------------------------------------- */
165
180
 
166
181
  /** Inline SVG placeholder for non-image files. */
167
182
  const PLACEHOLDER_SVG = 'data:image/svg+xml;utf8,' + encodeURIComponent(
@@ -241,7 +256,7 @@ function createUploadCard(f, uploadResult)
241
256
  return card;
242
257
  }
243
258
 
244
- /* ── Paginated Upload List ─────────────────────────────────────────────────── */
259
+ /* -- Paginated Upload List --------------------------------------------------- */
245
260
 
246
261
  /**
247
262
  * Fetch the paginated upload list and render file cards into `#uploadsList`.
@@ -310,7 +325,7 @@ async function loadUploadsList()
310
325
  }
311
326
  }
312
327
 
313
- /* ── Wire Upload Form & Bulk Actions ───────────────────────────────────────── */
328
+ /* -- Wire Upload Form & Bulk Actions ----------------------------------------- */
314
329
 
315
330
  /**
316
331
  * Initialise the upload form (XHR with progress), pagination controls, and
@@ -188,24 +188,29 @@ details.acc[open] > summary:before{transform:rotate(90deg)}
188
188
  .tabs-content{display:block}
189
189
  .tab-panel{display:none;padding:6px 2px;transition:opacity .18s ease}
190
190
  .tab-panel.active{display:block}
191
- .feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
192
- .feature-card{background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);padding:14px;border-radius:12px;border:1px solid rgba(255,255,255,0.03);display:flex;flex-direction:column;align-items:flex-start;gap:10px;transition:transform .12s ease,box-shadow .12s ease}
193
- .feature-card:hover{transform:translateY(-6px);box-shadow:0 12px 36px rgba(0,0,0,0.6)}
194
- .feature-icon{width:56px;height:56px;border-radius:12px;background:linear-gradient(180deg, rgba(88,101,242,0.12), rgba(31,142,253,0.06));display:grid;place-items:center;font-size:20px;color:var(--accent);}
195
- .feature-icon svg{width:34px;height:34px;display:block;fill:var(--accent);}
196
- .feature-icon img.icon-svg{width:40px;height:40px;display:block;object-fit:contain;margin:0 auto}
197
- /* .feature-icon img.icon-svg[src$="/fetch.svg"], .feature-icon img.icon-svg[src$="fetch.svg"]{transform:translateY(-5px)translateX(1px);display:block} */
198
- .feature-icon img.icon-svg[src$="/static.svg"], .feature-icon img.icon-svg[src$="static.svg"]{transform:translateY(2px);display:block}
199
- /* .feature-icon img.icon-svg[src$="/plug.svg"], .feature-icon img.icon-svg[src$="plug.svg"]{transform:translateY(2px);display:block} */
200
- .feature-card h5{margin:0;font-size:15px}
201
- .feature-card p{margin:0;color:var(--muted);font-size:14px}
202
- .behavior-list{display:flex;flex-direction:column;gap:10px}
203
- .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)}
204
- .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)}
205
- .step-body h5{margin:0 0 6px 0}
206
- .step-body p{margin:0;color:var(--muted)}
207
-
208
- @media(max-width:900px){.feature-grid{grid-template-columns:1fr}.tabs-nav{flex-wrap:wrap}}
191
+ .feature-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px}
192
+ .feature-card{background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.03);display:flex;flex-direction:row;align-items:center;gap:10px;transition:transform .12s ease,box-shadow .12s ease}
193
+ .feature-card:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,0.5)}
194
+ .feature-icon{width:40px;height:40px;min-width:40px;border-radius:10px;background:linear-gradient(180deg, rgba(88,101,242,0.12), rgba(31,142,253,0.06));display:grid;place-items:center;font-size:16px;color:var(--accent);}
195
+ .feature-icon svg{width:26px;height:26px;display:block;fill:var(--accent);}
196
+ .feature-icon img.icon-svg{width:30px;height:30px;display:block;object-fit:contain;margin:0 auto}
197
+ .feature-icon img.icon-svg[src$="/static.svg"], .feature-icon img.icon-svg[src$="static.svg"]{transform:translateY(1px);display:block}
198
+ .feature-card h5{margin:0;font-size:13px;line-height:1.2}
199
+ .feature-card p{margin:0;color:var(--muted);font-size:12px;line-height:1.3}
200
+ .feature-text{display:flex;flex-direction:column;gap:2px;min-width:0}
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)}}
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}}
211
216
 
@@ -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}
@@ -0,0 +1,27 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-comp" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Outer large container (uncompressed) -->
10
+ <rect x="6" y="8" width="24" height="48" rx="5" fill="url(#g-comp)" opacity="0.3"/>
11
+ <!-- Inward arrows implying compression -->
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
+ <!-- Arrow pointing to compressed result -->
19
+ <path d="M34 32h6" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round"/>
20
+ <path d="M38 28l4 4-4 4" fill="none" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
21
+ <!-- Compact compressed block -->
22
+ <rect x="44" y="20" width="14" height="24" rx="4" fill="url(#g-comp)"/>
23
+ <!-- White highlight lines on compressed block -->
24
+ <line x1="48" y1="27" x2="54" y2="27" stroke="rgba(255,255,255,0.8)" stroke-width="1.5" stroke-linecap="round"/>
25
+ <line x1="48" y1="32" x2="54" y2="32" stroke="rgba(255,255,255,0.55)" stroke-width="1.5" stroke-linecap="round"/>
26
+ <line x1="48" y1="37" x2="52" y2="37" stroke="rgba(255,255,255,0.35)" stroke-width="1.5" stroke-linecap="round"/>
27
+ </svg>
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-tls" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Shield shape -->
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
+ <!-- Lock body -->
12
+ <rect x="24" y="30" width="16" height="14" rx="3" fill="rgba(255,255,255,0.9)"/>
13
+ <!-- Lock shackle -->
14
+ <path d="M27 30 V24 C27 19 37 19 37 24 V30" fill="none" stroke="rgba(255,255,255,0.9)" stroke-width="3" stroke-linecap="round"/>
15
+ <!-- Keyhole -->
16
+ <circle cx="32" cy="36" r="2.5" fill="url(#g-tls)"/>
17
+ <rect x="31" y="36" width="2" height="4" rx="1" fill="url(#g-tls)"/>
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>
@@ -0,0 +1,27 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-rtr" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Central hub node -->
10
+ <circle cx="32" cy="32" r="10" fill="url(#g-rtr)"/>
11
+ <circle cx="32" cy="32" r="4" fill="rgba(255,255,255,0.85)"/>
12
+ <!-- Branch: top-left -->
13
+ <line x1="24" y1="24" x2="14" y2="14" stroke="url(#g-rtr)" stroke-width="2.5" stroke-linecap="round"/>
14
+ <rect x="6" y="6" width="14" height="12" rx="3.5" fill="url(#g-rtr)" opacity="0.7"/>
15
+ <line x1="10" y1="11" x2="16" y2="11" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
16
+ <line x1="10" y1="14" x2="14" y2="14" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round"/>
17
+ <!-- Branch: top-right -->
18
+ <line x1="40" y1="24" x2="50" y2="14" stroke="url(#g-rtr)" stroke-width="2.5" stroke-linecap="round"/>
19
+ <rect x="44" y="6" width="14" height="12" rx="3.5" fill="url(#g-rtr)" opacity="0.7"/>
20
+ <line x1="48" y1="11" x2="54" y2="11" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
21
+ <line x1="48" y1="14" x2="52" y2="14" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round"/>
22
+ <!-- Branch: bottom-center -->
23
+ <line x1="32" y1="42" x2="32" y2="50" stroke="url(#g-rtr)" stroke-width="2.5" stroke-linecap="round"/>
24
+ <rect x="22" y="50" width="20" height="10" rx="3.5" fill="url(#g-rtr)" opacity="0.7"/>
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
+ <line x1="26" y1="58" x2="34" y2="58" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round"/>
27
+ </svg>
@@ -0,0 +1,22 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-sse" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Server box -->
10
+ <rect x="8" y="16" width="20" height="32" rx="5" fill="url(#g-sse)"/>
11
+ <!-- White indicator dots on server -->
12
+ <circle cx="18" cy="25" r="2.5" fill="rgba(255,255,255,0.9)"/>
13
+ <circle cx="18" cy="32" r="2.5" fill="rgba(255,255,255,0.55)"/>
14
+ <circle cx="18" cy="39" r="2.5" fill="rgba(255,255,255,0.35)"/>
15
+ <!-- Three streaming arrows flowing right -->
16
+ <line x1="32" y1="24" x2="50" y2="24" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round"/>
17
+ <path d="M47 20l5 4-5 4" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
18
+ <line x1="34" y1="32" x2="52" y2="32" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round" opacity="0.7"/>
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
+ <line x1="36" y1="40" x2="54" y2="40" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round" opacity="0.45"/>
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
+ </svg>
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-ws" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Two speech bubbles overlapping to represent bidirectional real-time messaging -->
10
+ <rect x="6" y="10" width="32" height="22" rx="6" fill="url(#g-ws)"/>
11
+ <polygon points="14,32 20,40 24,32" fill="url(#g-ws)"/>
12
+ <!-- White dots in left bubble -->
13
+ <circle cx="15" cy="21" r="2.5" fill="rgba(255,255,255,0.85)"/>
14
+ <circle cx="22" cy="21" r="2.5" fill="rgba(255,255,255,0.85)"/>
15
+ <circle cx="29" cy="21" r="2.5" fill="rgba(255,255,255,0.85)"/>
16
+ <!-- Second bubble (offset) -->
17
+ <rect x="26" y="28" width="32" height="20" rx="6" fill="url(#g-ws)" opacity="0.75"/>
18
+ <polygon points="44,48 48,55 52,48" fill="url(#g-ws)" opacity="0.75"/>
19
+ <!-- Arrow inside second bubble implying response -->
20
+ <path d="M34 38h16M44 34l6 4-6 4" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
21
+ </svg>
package/index.js CHANGED
@@ -4,12 +4,16 @@
4
4
  * Re-exports every middleware, the app factory, and the fetch helper.
5
5
  */
6
6
  const App = require('./lib/app');
7
- const cors = require('./lib/cors');
7
+ const Router = require('./lib/router');
8
+ const cors = require('./lib/middleware/cors');
8
9
  const fetch = require('./lib/fetch');
9
10
  const body = require('./lib/body');
10
- const serveStatic = require('./lib/static');
11
- const rateLimit = require('./lib/rateLimit');
12
- const logger = require('./lib/logger');
11
+ const serveStatic = require('./lib/middleware/static');
12
+ const rateLimit = require('./lib/middleware/rateLimit');
13
+ const logger = require('./lib/middleware/logger');
14
+ const compress = require('./lib/middleware/compress');
15
+ const { WebSocketConnection } = require('./lib/ws');
16
+ const { SSEStream } = require('./lib/sse');
13
17
 
14
18
  module.exports = {
15
19
  /**
@@ -17,6 +21,12 @@ module.exports = {
17
21
  * @returns {import('./lib/app')} Fresh App with an empty middleware stack.
18
22
  */
19
23
  createApp: () => new App(),
24
+ /**
25
+ * Create a standalone Router for modular route grouping.
26
+ * Mount on an App with `app.use('/prefix', router)`.
27
+ * @returns {import('./lib/router')} Fresh Router instance.
28
+ */
29
+ Router: () => new Router(),
20
30
  /** @see module:cors */
21
31
  cors,
22
32
  /** @see module:fetch */
@@ -40,4 +50,11 @@ module.exports = {
40
50
  rateLimit,
41
51
  /** @see module:logger */
42
52
  logger,
53
+ /** @see module:compress */
54
+ compress,
55
+ // classes (for advanced / direct usage)
56
+ /** @see module:ws/connection */
57
+ WebSocketConnection,
58
+ /** @see module:sse/stream */
59
+ SSEStream,
43
60
  };