zero-http 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +28 -177
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. package/types/websocket.d.ts +119 -0
@@ -75,6 +75,12 @@ function initPlayground()
75
75
 
76
76
  /* --- SSE Viewer --- */
77
77
  initSseViewer();
78
+
79
+ /* --- ORM CRUD --- */
80
+ initOrmPlayground();
81
+
82
+ /* --- Cookie Explorer --- */
83
+ initCookieExplorer();
78
84
  }
79
85
 
80
86
  /* ------------------------------------------------------------------ */
@@ -273,3 +279,267 @@ function initSseViewer()
273
279
  appendMsg(`<span style="color:#aaa">» Broadcast sent to ${j.sent} client(s)</span>`);
274
280
  });
275
281
  }
282
+
283
+ /* ------------------------------------------------------------------ */
284
+ /* ORM Task Manager Playground */
285
+ /* ------------------------------------------------------------------ */
286
+ function initOrmPlayground()
287
+ {
288
+ const form = $('#taskForm');
289
+ const titleIn = $('#taskTitle');
290
+ const statusIn = $('#taskStatus');
291
+ const priorityIn = $('#taskPriority');
292
+ const searchIn = $('#taskSearch');
293
+ const scopeIn = $('#taskScope');
294
+ const delAllBtn = $('#taskDeleteAll');
295
+ const listEl = $('#taskList');
296
+ const resultEl = $('#taskResult');
297
+ const statsEl = $('#ormStats');
298
+
299
+ if (!form) return;
300
+
301
+ let editingId = null;
302
+
303
+ /* ---- helpers ---- */
304
+ function statusBadge(s)
305
+ {
306
+ const colors = { pending: '#fa0', 'in-progress': '#5865f2', done: '#2ecc71' };
307
+ return `<span style="padding:2px 8px;border-radius:4px;font-size:12px;background:${colors[s] || '#555'};color:#fff">${escapeHtml(s)}</span>`;
308
+ }
309
+
310
+ function priorityStars(n)
311
+ {
312
+ return '<span style="color:#fa0">' + '★'.repeat(n) + '</span>' + '<span style="color:#333">' + '☆'.repeat(5 - n) + '</span>';
313
+ }
314
+
315
+ async function loadStats()
316
+ {
317
+ try
318
+ {
319
+ const r = await fetch('/api/tasks/stats');
320
+ const s = await r.json();
321
+ statsEl.innerHTML =
322
+ `<div class="orm-stat-grid">` +
323
+ `<div class="orm-stat"><span class="orm-stat-val">${s.total}</span><span class="orm-stat-label">Total</span></div>` +
324
+ `<div class="orm-stat"><span class="orm-stat-val" style="color:#fa0">${s.pending}</span><span class="orm-stat-label">Pending</span></div>` +
325
+ `<div class="orm-stat"><span class="orm-stat-val" style="color:#5865f2">${s.inProgress}</span><span class="orm-stat-label">In-progress</span></div>` +
326
+ `<div class="orm-stat"><span class="orm-stat-val" style="color:#2ecc71">${s.done}</span><span class="orm-stat-label">Done</span></div>` +
327
+ `<div class="orm-stat"><span class="orm-stat-val">${Number(s.avgPriority).toFixed(1)}</span><span class="orm-stat-label">Avg Priority</span></div>` +
328
+ `</div>`;
329
+ } catch (e) { }
330
+ }
331
+
332
+ async function loadTasks()
333
+ {
334
+ try
335
+ {
336
+ const params = new URLSearchParams();
337
+ if (searchIn.value.trim()) params.set('search', searchIn.value.trim());
338
+ if (scopeIn.value) params.set('scope', scopeIn.value);
339
+ params.set('sort', 'createdAt');
340
+ params.set('order', 'desc');
341
+
342
+ const r = await fetch('/api/tasks?' + params);
343
+ const data = await r.json();
344
+ showJsonResult(resultEl, data);
345
+
346
+ if (!data.tasks || !data.tasks.length)
347
+ {
348
+ listEl.innerHTML = '<div style="padding:12px;color:#98a0aa">No tasks yet — add one above.</div>';
349
+ loadStats();
350
+ return;
351
+ }
352
+
353
+ listEl.innerHTML = data.tasks.map(t =>
354
+ `<div class="task-row" data-id="${t.id}">` +
355
+ `<div class="task-info">` +
356
+ `<strong>${escapeHtml(t.title)}</strong> ` +
357
+ statusBadge(t.status) + ' ' + priorityStars(t.priority) +
358
+ `<div class="small muted">ID ${t.id} · ${new Date(t.createdAt).toLocaleString()}</div>` +
359
+ `</div>` +
360
+ `<div class="task-actions">` +
361
+ `<button class="btn small task-edit-btn" data-id="${t.id}">Edit</button>` +
362
+ `<button class="btn small task-cycle-btn" data-id="${t.id}" data-status="${t.status}" title="Cycle status">↻</button>` +
363
+ `<button class="btn small warn task-del-btn" data-id="${t.id}">Delete</button>` +
364
+ `</div>` +
365
+ `</div>`
366
+ ).join('');
367
+
368
+ loadStats();
369
+ }
370
+ catch (e) { listEl.innerHTML = `<div style="color:#f66">${escapeHtml(e.message)}</div>`; }
371
+ }
372
+
373
+ /* ---- events ---- */
374
+ on(form, 'submit', async (e) =>
375
+ {
376
+ e.preventDefault();
377
+ const body = {
378
+ title: titleIn.value.trim(),
379
+ status: statusIn.value,
380
+ priority: Number(priorityIn.value),
381
+ };
382
+ if (!body.title) return;
383
+
384
+ if (editingId)
385
+ {
386
+ await fetch('/api/tasks/' + editingId, {
387
+ method: 'PUT',
388
+ headers: { 'Content-Type': 'application/json' },
389
+ body: JSON.stringify(body),
390
+ });
391
+ editingId = null;
392
+ form.querySelector('button[type="submit"]').textContent = 'Add Task';
393
+ }
394
+ else
395
+ {
396
+ await fetch('/api/tasks', {
397
+ method: 'POST',
398
+ headers: { 'Content-Type': 'application/json' },
399
+ body: JSON.stringify(body),
400
+ });
401
+ }
402
+ titleIn.value = '';
403
+ statusIn.value = 'pending';
404
+ priorityIn.value = '0';
405
+ loadTasks();
406
+ });
407
+
408
+ on(listEl, 'click', async (e) =>
409
+ {
410
+ const btn = e.target.closest('button');
411
+ if (!btn) return;
412
+ const id = btn.dataset.id;
413
+
414
+ if (btn.classList.contains('task-del-btn'))
415
+ {
416
+ await fetch('/api/tasks/' + id, { method: 'DELETE' });
417
+ loadTasks();
418
+ }
419
+ else if (btn.classList.contains('task-cycle-btn'))
420
+ {
421
+ const next = { pending: 'in-progress', 'in-progress': 'done', done: 'pending' };
422
+ await fetch('/api/tasks/' + id, {
423
+ method: 'PUT',
424
+ headers: { 'Content-Type': 'application/json' },
425
+ body: JSON.stringify({ status: next[btn.dataset.status] || 'pending' }),
426
+ });
427
+ loadTasks();
428
+ }
429
+ else if (btn.classList.contains('task-edit-btn'))
430
+ {
431
+ editingId = id;
432
+ const row = btn.closest('.task-row');
433
+ const title = row.querySelector('strong').textContent;
434
+ titleIn.value = title;
435
+ form.querySelector('button[type="submit"]').textContent = 'Save';
436
+ titleIn.focus();
437
+ }
438
+ });
439
+
440
+ let searchTimer;
441
+ on(searchIn, 'input', () => { clearTimeout(searchTimer); searchTimer = setTimeout(loadTasks, 300); });
442
+ on(scopeIn, 'change', loadTasks);
443
+
444
+ on(delAllBtn, 'click', async () =>
445
+ {
446
+ await fetch('/api/tasks', { method: 'DELETE' });
447
+ loadTasks();
448
+ });
449
+
450
+ loadTasks();
451
+ }
452
+
453
+ /* ------------------------------------------------------------------ */
454
+ /* Cookie Explorer Playground */
455
+ /* ------------------------------------------------------------------ */
456
+ function initCookieExplorer()
457
+ {
458
+ const form = $('#cookieForm');
459
+ const nameIn = $('#cookieName');
460
+ const valueIn = $('#cookieValue');
461
+ const httpOnlyIn = $('#cookieHttpOnly');
462
+ const sameSiteIn = $('#cookieSameSite');
463
+ const maxAgeIn = $('#cookieMaxAge');
464
+ const refreshBtn = $('#cookieRefresh');
465
+ const jarEl = $('#cookieJar');
466
+ const resultEl = $('#cookieResult');
467
+
468
+ if (!form) return;
469
+
470
+ async function loadCookies()
471
+ {
472
+ try
473
+ {
474
+ const r = await fetch('/api/cookies');
475
+ const data = await r.json();
476
+ showJsonResult(resultEl, data);
477
+
478
+ const all = Object.entries(data.cookies || {});
479
+ const signed = Object.entries(data.signedCookies || {});
480
+
481
+ if (!all.length && !signed.length)
482
+ {
483
+ jarEl.innerHTML = '<div style="padding:12px;color:#98a0aa">No cookies set — use the form above to create one.</div>';
484
+ return;
485
+ }
486
+
487
+ let html = '';
488
+ for (const [name, val] of all)
489
+ {
490
+ html += `<div class="cookie-row">` +
491
+ `<span class="cookie-name">${escapeHtml(name)}</span>` +
492
+ `<span class="cookie-val">${escapeHtml(typeof val === 'object' ? JSON.stringify(val) : String(val))}</span>` +
493
+ `<button class="btn small warn cookie-del-btn" data-name="${escapeHtml(name)}">Clear</button>` +
494
+ `</div>`;
495
+ }
496
+ for (const [name, val] of signed)
497
+ {
498
+ html += `<div class="cookie-row">` +
499
+ `<span class="cookie-name">${escapeHtml(name)} <span style="color:#5865f2;font-size:11px">signed</span></span>` +
500
+ `<span class="cookie-val">${escapeHtml(typeof val === 'object' ? JSON.stringify(val) : String(val))}</span>` +
501
+ `<button class="btn small warn cookie-del-btn" data-name="${escapeHtml(name)}">Clear</button>` +
502
+ `</div>`;
503
+ }
504
+ jarEl.innerHTML = html;
505
+ }
506
+ catch (e) { jarEl.innerHTML = `<div style="color:#f66">${escapeHtml(e.message)}</div>`; }
507
+ }
508
+
509
+ on(form, 'submit', async (e) =>
510
+ {
511
+ e.preventDefault();
512
+ const name = nameIn.value.trim();
513
+ if (!name) return;
514
+
515
+ const options = { sameSite: sameSiteIn.value };
516
+ if (httpOnlyIn.value === 'true') options.httpOnly = true;
517
+ if (maxAgeIn.value) options.maxAge = Number(maxAgeIn.value);
518
+
519
+ await fetch('/api/cookies', {
520
+ method: 'POST',
521
+ headers: { 'Content-Type': 'application/json' },
522
+ body: JSON.stringify({
523
+ name,
524
+ value: valueIn.value,
525
+ options,
526
+ }),
527
+ });
528
+ nameIn.value = '';
529
+ valueIn.value = '';
530
+ maxAgeIn.value = '';
531
+ loadCookies();
532
+ });
533
+
534
+ on(jarEl, 'click', async (e) =>
535
+ {
536
+ const btn = e.target.closest('.cookie-del-btn');
537
+ if (!btn) return;
538
+ await fetch('/api/cookies/' + encodeURIComponent(btn.dataset.name), { method: 'DELETE' });
539
+ loadCookies();
540
+ });
541
+
542
+ on(refreshBtn, 'click', loadCookies);
543
+
544
+ loadCookies();
545
+ }
@@ -127,11 +127,11 @@ function initTocNavigation()
127
127
  */
128
128
  function openAncestors(el)
129
129
  {
130
- let d = el.closest('details.acc');
130
+ let d = el.closest('details');
131
131
  while (d)
132
132
  {
133
133
  d.open = true;
134
- d = d.parentElement ? d.parentElement.closest('details.acc') : null;
134
+ d = d.parentElement ? d.parentElement.closest('details') : null;
135
135
  }
136
136
  }
137
137
 
@@ -229,7 +229,8 @@ function initTocCollapsible()
229
229
  const items = document.querySelectorAll('.toc-collapsible');
230
230
  items.forEach(li =>
231
231
  {
232
- /* Start expanded */
232
+ /* Skip if already has a toggle button */
233
+ if (li.querySelector('.toc-collapse-btn')) return;
233
234
 
234
235
  /* Create toggle button */
235
236
  const toggle = document.createElement('button');
@@ -71,9 +71,23 @@ textarea{min-height:100px}
71
71
 
72
72
  /* Controls: select and pager */
73
73
  .selectWrap{display:inline-flex;align-items:center;gap:8px;background:transparent}
74
- .selectWrap select, #sortSelect, #sortOrder{appearance:none;-webkit-appearance:none;border:1px solid rgba(255,255,255,0.06);background:var(--panel);color:var(--text);padding:8px 12px;border-radius:10px;font-size:14px;min-width:110px}
75
- .selectWrap select option, #sortSelect option, #sortOrder option{background:var(--panel);color:var(--text)}
76
- .selectWrap select:focus, #sortSelect:focus, #sortOrder:focus{outline:none;box-shadow:0 0 0 4px rgba(88,101,242,0.12)}
74
+
75
+ /* Custom select */
76
+ .custom-select{position:relative;display:inline-block;min-width:110px;font-size:14px;user-select:none}
77
+ .custom-select .cs-trigger{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.06);background:var(--panel);color:var(--text);cursor:pointer;transition:border-color .15s,box-shadow .15s;white-space:nowrap}
78
+ .custom-select .cs-trigger:hover{border-color:rgba(88,101,242,0.3)}
79
+ .custom-select.open .cs-trigger{border-color:rgba(88,101,242,0.4);box-shadow:0 0 0 4px rgba(88,101,242,0.12)}
80
+ .custom-select .cs-arrow{width:10px;height:10px;flex-shrink:0;transition:transform .2s ease}
81
+ .custom-select.open .cs-arrow{transform:rotate(180deg)}
82
+ .custom-select .cs-dropdown{position:absolute;top:calc(100% + 4px);left:0;right:0;min-width:100%;width:max-content;max-height:220px;overflow-y:auto;background:var(--panel);border:1px solid rgba(255,255,255,0.08);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.5);z-index:200;padding:4px;opacity:0;visibility:hidden;transform:translateY(-4px);transition:opacity .15s,transform .15s,visibility .15s}
83
+ .custom-select.open .cs-dropdown{opacity:1;visibility:visible;transform:translateY(0)}
84
+ .custom-select .cs-option{padding:7px 12px;border-radius:7px;color:var(--text);cursor:pointer;transition:background .1s;white-space:nowrap}
85
+ .custom-select .cs-option:hover{background:rgba(88,101,242,0.15)}
86
+ .custom-select .cs-option.selected{background:rgba(88,101,242,0.2);font-weight:600}
87
+ .custom-select .cs-dropdown::-webkit-scrollbar{width:5px}
88
+ .custom-select .cs-dropdown::-webkit-scrollbar-track{background:transparent}
89
+ .custom-select .cs-dropdown::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:3px}
90
+ select.cs-hidden{position:absolute;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none}
77
91
 
78
92
  .pager{display:flex;align-items:center;gap:10px}
79
93
  .pager button{min-width:36px;height:36px;padding:0;border-radius:10px;background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);border:1px solid rgba(255,255,255,0.03);display:inline-flex;align-items:center;justify-content:center}
@@ -195,7 +209,7 @@ details.acc[open] > summary:before{transform:rotate(90deg)}
195
209
  .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);}
196
210
  .feature-icon svg{width:26px;height:26px;display:block;fill:var(--accent);}
197
211
  .feature-icon img.icon-svg{width:30px;height:30px;display:block;object-fit:contain;margin:0 auto}
198
- .feature-icon img.icon-svg[src$="/static.svg"], .feature-icon img.icon-svg[src$="static.svg"]{transform:translateY(1px);display:block}
212
+ /* .feature-icon img.icon-svg[src$="/static.svg"], .feature-icon img.icon-svg[src$="static.svg"]{transform:translateY(1px);display:block} */
199
213
  .feature-card h5{margin:0;font-size:13px;line-height:1.2}
200
214
  .feature-card p{margin:0;color:var(--muted);font-size:12px;line-height:1.3}
201
215
  .feature-text{display:flex;flex-direction:column;gap:2px;min-width:0}
@@ -331,4 +345,41 @@ body{padding-top:calc(var(--header-height) + 12px)}
331
345
  .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
346
  @keyframes toast-in{from{opacity:0;transform:translateX(-50%) translateY(-10px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
333
347
  .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}
348
+ .undo-toast-close:hover{opacity:1}
349
+
350
+ /* -- Documentation sections -------------------------------------------------- */
351
+ .doc-sections-container{margin-top:8px}
352
+ .doc-section{margin-bottom:28px;padding:0}
353
+ .doc-section-header{display:flex;align-items:center;gap:10px;margin-bottom:6px;padding:12px 0 8px}
354
+ .doc-section-icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,rgba(88,101,242,0.15),rgba(31,142,253,0.08));color:var(--accent);flex-shrink:0}
355
+ .doc-section-icon svg{width:16px;height:16px;display:block}
356
+ .doc-section-title{margin:0;font-size:1.15rem;font-weight:800;letter-spacing:-0.01em}
357
+ .doc-section-divider{height:1px;background:linear-gradient(90deg,var(--accent) 0%,rgba(88,101,242,0.2) 30%,transparent 100%);margin-bottom:14px}
358
+
359
+ /* Tips list */
360
+ .doc-tips{margin-top:14px;padding:12px 14px;background:linear-gradient(180deg,rgba(88,101,242,0.06),rgba(88,101,242,0.02));border:1px solid rgba(88,101,242,0.1);border-radius:10px}
361
+ .doc-tips-heading{margin:0 0 8px;color:var(--accent);font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em}
362
+ .tips-list{margin:0;padding:0 0 0 18px;display:flex;flex-direction:column;gap:6px}
363
+ .tips-list li{font-size:13.5px;line-height:1.55;color:var(--text)}
364
+ .tips-list li::marker{color:var(--accent)}
365
+
366
+ /* Section-level h6 headings */
367
+ .doc-item .acc-body h6{margin:16px 0 6px;font-size:13px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:0.04em}
368
+
369
+ /* ORM Playground */
370
+ .orm-stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px}
371
+ .orm-stat{display:flex;flex-direction:column;align-items:center;padding:10px 18px;border-radius:8px;background:rgba(88,101,242,.08);border:1px solid rgba(88,101,242,.15);min-width:80px}
372
+ .orm-stat-val{font-size:22px;font-weight:800;color:var(--text)}
373
+ .orm-stat-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-top:2px}
374
+ .orm-toolbar{margin-bottom:4px}
375
+ .task-row{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.06)}
376
+ .task-row:last-child{border-bottom:none}
377
+ .task-info{display:flex;flex-wrap:wrap;gap:6px;align-items:center;flex:1;min-width:0}
378
+ .task-info strong{margin-right:4px}
379
+ .task-actions{display:flex;gap:4px;flex-shrink:0;margin-left:8px}
380
+
381
+ /* Cookie Explorer */
382
+ .cookie-row{display:flex;align-items:center;gap:10px;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.06)}
383
+ .cookie-row:last-child{border-bottom:none}
384
+ .cookie-name{font-weight:700;color:var(--accent);min-width:80px}
385
+ .cookie-val{flex:1;word-break:break-all;color:var(--text)}
@@ -6,22 +6,22 @@
6
6
  </linearGradient>
7
7
  </defs>
8
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)"/>
9
+ <!-- Outer block (uncompressed) -->
10
+ <rect x="6" y="6" width="52" height="52" rx="8" fill="url(#g-comp)" opacity="0.2"/>
11
+ <!-- Inner compressed block -->
12
+ <rect x="18" y="18" width="28" height="28" rx="6" fill="url(#g-comp)"/>
23
13
  <!-- 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"/>
14
+ <line x1="24" y1="28" x2="40" y2="28" stroke="rgba(255,255,255,0.8)" stroke-width="2" stroke-linecap="round"/>
15
+ <line x1="24" y1="33" x2="38" y2="33" stroke="rgba(255,255,255,0.5)" stroke-width="2" stroke-linecap="round"/>
16
+ <line x1="24" y1="38" x2="34" y2="38" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-linecap="round"/>
17
+ <!-- Inward squeeze arrows -->
18
+ <path d="M10 16l8 8" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round"/>
19
+ <path d="M54 16l-8 8" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round"/>
20
+ <path d="M10 48l8-8" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round"/>
21
+ <path d="M54 48l-8-8" stroke="url(#g-comp)" stroke-width="2.5" stroke-linecap="round"/>
22
+ <!-- Arrow tips -->
23
+ <path d="M18 24l-2-6 6 2" fill="none" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
24
+ <path d="M46 24l2-6-6 2" fill="none" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
25
+ <path d="M18 40l-2 6 6-2" fill="none" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
26
+ <path d="M46 40l2 6-6-2" fill="none" stroke="url(#g-comp)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
27
27
  </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-db" 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
+ <!-- Database cylinder -->
10
+ <ellipse cx="32" cy="14" rx="20" ry="8" fill="url(#g-db)"/>
11
+ <path d="M12 14v36c0 4.4 9 8 20 8s20-3.6 20-8V14" fill="url(#g-db)" opacity="0.5"/>
12
+ <ellipse cx="32" cy="50" rx="20" ry="8" fill="url(#g-db)" opacity="0.6"/>
13
+ <!-- Middle ring -->
14
+ <path d="M12 32c0 4.4 9 8 20 8s20-3.6 20-8" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1.5"/>
15
+ <!-- Top highlight -->
16
+ <ellipse cx="32" cy="14" rx="14" ry="4.5" fill="rgba(255,255,255,0.15)"/>
17
+ <!-- Row dots -->
18
+ <circle cx="26" cy="24" r="2" fill="rgba(255,255,255,0.7)"/>
19
+ <circle cx="32" cy="24" r="2" fill="rgba(255,255,255,0.5)"/>
20
+ <circle cx="38" cy="24" r="2" fill="rgba(255,255,255,0.3)"/>
21
+ </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-env" 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
+ <!-- Stacked layers -->
10
+ <path d="M32 38L6 26l26-14 26 14L32 38z" fill="url(#g-env)" opacity="0.3"/>
11
+ <path d="M32 46L6 34l26-14 26 14L32 46z" fill="url(#g-env)" opacity="0.55"/>
12
+ <path d="M32 54L6 42l26-14 26 14L32 54z" fill="url(#g-env)"/>
13
+ <!-- Top layer highlight -->
14
+ <path d="M32 48L14 40l18-10 18 10L32 48z" fill="rgba(255,255,255,0.12)"/>
15
+ <!-- Gear accent on top layer -->
16
+ <circle cx="32" cy="22" r="4" fill="none" stroke="rgba(255,255,255,0.7)" stroke-width="1.5"/>
17
+ <line x1="32" y1="16" x2="32" y2="18" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
18
+ <line x1="32" y1="26" x2="32" y2="28" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
19
+ <line x1="26" y1="22" x2="28" y2="22" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
20
+ <line x1="36" y1="22" x2="38" y2="22" stroke="rgba(255,255,255,0.7)" stroke-width="1.5" stroke-linecap="round"/>
21
+ </svg>
@@ -4,20 +4,17 @@
4
4
  <stop offset="0" stop-color="#7b61ff"/>
5
5
  <stop offset="1" stop-color="#3ec6ff"/>
6
6
  </linearGradient>
7
- <!-- Cloud silhouette via clipPath so gradient is seamless across the whole shape -->
8
- <clipPath id="cloud-clip">
9
- <circle cx="18" cy="26" r="11"/>
10
- <circle cx="32" cy="17" r="14"/>
11
- <circle cx="46" cy="24" r="11"/>
12
- <rect x="8" y="28" width="48" height="12" rx="6"/>
13
- </clipPath>
14
7
  </defs>
15
8
  <rect width="64" height="64" rx="8" fill="transparent"/>
16
-
17
- <!-- Large centered cloud filled with flat gradient, clipped to cloud silhouette -->
18
- <rect x="0" y="0" width="64" height="64" fill="url(#g-fetch)" clip-path="url(#cloud-clip)"/>
19
-
20
- <!-- Centered down arrow below the cloud -->
21
- <path d="M32 43v13" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round"/>
22
- <path d="M27 56l5 5 5-5" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
9
+ <!-- Globe/sphere -->
10
+ <circle cx="32" cy="28" r="20" fill="url(#g-fetch)" opacity="0.3"/>
11
+ <circle cx="32" cy="28" r="16" fill="url(#g-fetch)"/>
12
+ <!-- Globe meridians -->
13
+ <ellipse cx="32" cy="28" rx="8" ry="16" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="1.5"/>
14
+ <path d="M16 28h32" stroke="rgba(255,255,255,0.3)" stroke-width="1.5"/>
15
+ <path d="M18 20h28" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
16
+ <path d="M18 36h28" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
17
+ <!-- Download arrow below globe -->
18
+ <path d="M32 46v12" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round"/>
19
+ <path d="M27 54l5 5 5-5" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
23
20
  </svg>
@@ -0,0 +1,15 @@
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-sec" 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 outline -->
10
+ <path d="M32 6L10 16v16c0 14 10 22 22 26 12-4 22-12 22-26V16L32 6z" fill="url(#g-sec)" opacity="0.25"/>
11
+ <!-- Shield solid inner -->
12
+ <path d="M32 12L16 20v12c0 10 7 17 16 20 9-3 16-10 16-20V20L32 12z" fill="url(#g-sec)"/>
13
+ <!-- Checkmark inside shield -->
14
+ <path d="M24 33l6 6 10-12" fill="none" stroke="#ffffff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
15
+ </svg>
@@ -6,17 +6,16 @@
6
6
  </linearGradient>
7
7
  </defs>
8
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"/>
9
+ <!-- Broadcast tower base -->
10
+ <path d="M24 58h16" stroke="url(#g-sse)" stroke-width="3" stroke-linecap="round"/>
11
+ <path d="M28 58L32 28L36 58" fill="url(#g-sse)" opacity="0.8"/>
12
+ <!-- Tower mast -->
13
+ <rect x="30" y="22" width="4" height="36" rx="2" fill="url(#g-sse)"/>
14
+ <!-- Signal dot -->
15
+ <circle cx="32" cy="20" r="4" fill="url(#g-sse)"/>
16
+ <circle cx="32" cy="20" r="2" fill="rgba(255,255,255,0.9)"/>
17
+ <!-- Broadcast waves -->
18
+ <path d="M22 14a14 14 0 0 1 20 0" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round"/>
19
+ <path d="M16 8a22 22 0 0 1 32 0" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
20
+ <path d="M10 2a30 30 0 0 1 44 0" fill="none" stroke="url(#g-sse)" stroke-width="2.5" stroke-linecap="round" opacity="0.3"/>
22
21
  </svg>
@@ -6,30 +6,16 @@
6
6
  </linearGradient>
7
7
  </defs>
8
8
  <rect width="64" height="64" rx="8" fill="transparent"/>
9
-
10
- <!-- Antenna arms from top-center -->
11
- <line x1="32" y1="10" x2="22" y2="1" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.92"/>
12
- <line x1="32" y1="10" x2="42" y2="1" stroke="#ffffff" stroke-width="2" stroke-linecap="round" opacity="0.92"/>
13
-
14
- <!-- TV outer body -->
15
- <rect x="6" y="10" width="52" height="34" rx="6" fill="url(#g-static)"/>
16
-
17
- <!-- Screen (dark inset) -->
18
- <rect x="11" y="14" width="42" height="22" rx="3" fill="rgba(8,10,20,0.5)"/>
19
-
20
- <!-- Static zig-zag noise lines on screen -->
21
- <g fill="none" stroke="#ffffff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" opacity="0.82">
22
- <path d="M13 20 L17 24 L21 20 L25 24 L29 20 L33 24 L37 20 L41 24 L45 20 L49 24"/>
23
- <path d="M13 29 L17 33 L21 29 L25 33 L29 29 L33 33 L37 29 L41 33 L45 29 L49 33"/>
24
- </g>
25
-
26
- <!-- Control knobs below screen (inside body) -->
27
- <circle cx="20" cy="41" r="2.2" fill="rgba(255,255,255,0.55)"/>
28
- <circle cx="32" cy="41" r="2.2" fill="rgba(255,255,255,0.25)"/>
29
- <circle cx="44" cy="41" r="2.2" fill="rgba(255,255,255,0.15)"/>
30
-
31
- <!-- Stand neck -->
32
- <rect x="27" y="44" width="10" height="5" rx="2" fill="url(#g-static)"/>
33
- <!-- Stand base -->
34
- <rect x="17" y="49" width="30" height="5" rx="3" fill="url(#g-static)"/>
9
+ <!-- Back file -->
10
+ <rect x="16" y="10" width="36" height="44" rx="4" fill="url(#g-static)" opacity="0.3"/>
11
+ <!-- Middle file -->
12
+ <rect x="12" y="14" width="36" height="44" rx="4" fill="url(#g-static)" opacity="0.55"/>
13
+ <!-- Front file -->
14
+ <rect x="8" y="18" width="36" height="44" rx="4" fill="url(#g-static)"/>
15
+ <!-- Content lines on front file -->
16
+ <line x1="15" y1="28" x2="37" y2="28" stroke="rgba(255,255,255,0.85)" stroke-width="2.5" stroke-linecap="round"/>
17
+ <line x1="15" y1="35" x2="33" y2="35" stroke="rgba(255,255,255,0.55)" stroke-width="2.5" stroke-linecap="round"/>
18
+ <line x1="15" y1="42" x2="28" y2="42" stroke="rgba(255,255,255,0.3)" stroke-width="2.5" stroke-linecap="round"/>
19
+ <!-- Small play/serve triangle badge -->
20
+ <path d="M48 46l10 7-10 7z" fill="url(#g-static)" opacity="0.7"/>
35
21
  </svg>
@@ -6,17 +6,11 @@
6
6
  </linearGradient>
7
7
  </defs>
8
8
  <rect width="64" height="64" rx="8" fill="transparent"/>
9
-
10
- <!-- Hard disk on the right, big thin white arrow pointing into it from the left -->
11
- <!-- Hard disk body -->
12
- <rect x="34" y="14" width="22" height="36" rx="5" fill="url(#g-stream)"/>
13
- <!-- disk platter slot line -->
14
- <line x1="38" y1="26" x2="52" y2="26" stroke="rgba(255,255,255,0.25)" stroke-width="1.5" stroke-linecap="round"/>
15
- <!-- small read-head dot -->
16
- <circle cx="44" cy="38" r="3" fill="rgba(255,255,255,0.2)"/>
17
- <circle cx="44" cy="38" r="1.4" fill="rgba(255,255,255,0.55)"/>
18
-
19
- <!-- Big thin white arrow pointing right into the disk -->
20
- <path d="M6 32h22" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
21
- <path d="M22 25l12 7-12 7" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
9
+ <!-- Document page -->
10
+ <path d="M14 4h24l14 14v38a4 4 0 0 1-4 4H14a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4z" fill="url(#g-stream)"/>
11
+ <!-- Folded corner -->
12
+ <path d="M38 4v10a4 4 0 0 0 4 4h10" fill="rgba(255,255,255,0.2)"/>
13
+ <!-- Bold upload arrow centered on page -->
14
+ <path d="M32 48V28" stroke="#ffffff" stroke-width="3.5" stroke-linecap="round"/>
15
+ <path d="M23 36l9-10 9 10" fill="none" stroke="#ffffff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
22
16
  </svg>