@x12i/connectix-docs 0.1.0 → 0.1.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 (67) hide show
  1. package/dist/generated/bundle.d.ts.map +1 -1
  2. package/dist/generated/bundle.js +50 -8
  3. package/dist/generated/bundle.js.map +1 -1
  4. package/dist/web/INDEX.md +38 -0
  5. package/dist/web/SEARCH.json +157 -0
  6. package/dist/web/agent/INDEX.md +38 -0
  7. package/dist/web/agent/SEARCH.json +157 -0
  8. package/dist/web/agent/agent-manifest.json +120 -0
  9. package/dist/web/agent/indexes/books.json +17 -0
  10. package/dist/web/agent/indexes/books.md +4 -0
  11. package/dist/web/agent/indexes/concepts.json +4 -0
  12. package/dist/web/agent/indexes/concepts.md +3 -0
  13. package/dist/web/agent/indexes/diagrams.json +4 -0
  14. package/dist/web/agent/indexes/diagrams.md +3 -0
  15. package/dist/web/agent/indexes/guides.json +4 -0
  16. package/dist/web/agent/indexes/guides.md +3 -0
  17. package/dist/web/agent/indexes/scenarios.json +4 -0
  18. package/dist/web/agent/indexes/scenarios.md +3 -0
  19. package/dist/web/agent/indexes/tutorials.json +4 -0
  20. package/dist/web/agent/indexes/tutorials.md +3 -0
  21. package/dist/web/agent/indexes/use-cases.json +29 -0
  22. package/dist/web/agent/indexes/use-cases.md +6 -0
  23. package/dist/web/agent/use-cases/call-connect-tools.md +78 -0
  24. package/dist/web/agent/use-cases/catalog-extension-overview.md +42 -0
  25. package/dist/web/agent/use-cases/orient-connectix.md +95 -0
  26. package/dist/web/agent/use-cases/run-package-simulator.md +48 -0
  27. package/dist/web/agent-manifest.json +20 -2
  28. package/dist/web/agents.html +38 -0
  29. package/dist/web/app.js +449 -0
  30. package/dist/web/assets/extras.css +302 -0
  31. package/dist/web/assets/site.css +21 -0
  32. package/dist/web/books/00-orient-connectix.html +35 -2
  33. package/dist/web/books/01-tools-and-simulator.html +35 -2
  34. package/dist/web/catalog.html +38 -0
  35. package/dist/web/concepts.html +65 -0
  36. package/dist/web/diagrams.html +65 -0
  37. package/dist/web/docs.html +38 -0
  38. package/dist/web/explore.html +76 -0
  39. package/dist/web/getting-started.html +38 -0
  40. package/dist/web/guides.html +65 -0
  41. package/dist/web/index.html +39 -1
  42. package/dist/web/indexes/books.json +17 -0
  43. package/dist/web/indexes/books.md +4 -0
  44. package/dist/web/indexes/concepts.json +4 -0
  45. package/dist/web/indexes/concepts.md +3 -0
  46. package/dist/web/indexes/diagrams.json +4 -0
  47. package/dist/web/indexes/diagrams.md +3 -0
  48. package/dist/web/indexes/guides.json +4 -0
  49. package/dist/web/indexes/guides.md +3 -0
  50. package/dist/web/indexes/scenarios.json +4 -0
  51. package/dist/web/indexes/scenarios.md +3 -0
  52. package/dist/web/indexes/tutorials.json +4 -0
  53. package/dist/web/indexes/tutorials.md +3 -0
  54. package/dist/web/indexes/use-cases.json +29 -0
  55. package/dist/web/indexes/use-cases.md +6 -0
  56. package/dist/web/knowledge.json +15 -0
  57. package/dist/web/read.html +161 -74
  58. package/dist/web/scenarios.html +65 -0
  59. package/dist/web/search.html +71 -0
  60. package/dist/web/site.json +1 -1
  61. package/dist/web/use-cases/call-connect-tools.md +78 -0
  62. package/dist/web/use-cases/catalog-extension-overview.md +42 -0
  63. package/dist/web/use-cases/orient-connectix.md +95 -0
  64. package/dist/web/use-cases/run-package-simulator.md +48 -0
  65. package/dist/web/use-cases.html +38 -0
  66. package/dist/web/use-cases.json +28 -4
  67. package/package.json +7 -7
package/dist/web/app.js CHANGED
@@ -22,6 +22,20 @@ function loadSite() {
22
22
  return loadJson("/site.json");
23
23
  }
24
24
 
25
+ function loadSequences() {
26
+ return loadJson("/sequences.json").then((d) => d.sequences || d);
27
+ }
28
+
29
+ function loadChapters(bookId, audience) {
30
+ return loadJson(
31
+ "/downloads/books/" +
32
+ encodeURIComponent(bookId) +
33
+ "/" +
34
+ encodeURIComponent(audience) +
35
+ ".chapters.json",
36
+ );
37
+ }
38
+
25
39
  function escapeHtml(s) {
26
40
  return String(s).replace(/[&<>"']/g, (c) =>
27
41
  ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c],
@@ -149,6 +163,135 @@ function hydrateUseCases(root) {
149
163
  });
150
164
  }
151
165
 
166
+ function readHref(bookId, audience, chapter) {
167
+ return (
168
+ "/read?book=" +
169
+ encodeURIComponent(bookId) +
170
+ "&role=" +
171
+ encodeURIComponent(audience) +
172
+ (chapter ? "#" + encodeURIComponent(chapter) : "")
173
+ );
174
+ }
175
+
176
+ /* Compact chapter index into one book (tutorials section). */
177
+ function hydrateTutorials(section) {
178
+ const bookId = section.getAttribute("data-book");
179
+ const audience = section.getAttribute("data-audience") || "developers";
180
+ const root = section.querySelector(".tutorials-root");
181
+ if (!bookId || !root) return;
182
+ loadChapters(bookId, audience)
183
+ .then((chapters) => {
184
+ const items = (chapters || [])
185
+ .filter((c) => c.slug !== "preamble")
186
+ .map(
187
+ (c, i) =>
188
+ `<li><span class="step-num">${i + 1}</span><span class="step-text"><a href="${readHref(bookId, audience, c.slug)}">${escapeHtml(c.title)}</a></span></li>`,
189
+ )
190
+ .join("");
191
+ root.innerHTML = `<ol class="step-list tutorials-list">${items}</ol>`;
192
+ })
193
+ .catch(() => {
194
+ /* keep the static "Start reading" fallback */
195
+ });
196
+ }
197
+
198
+ /* Interactive journey: ordered steps (required vs optional) → book chapters. */
199
+ function hydrateJourney(root) {
200
+ Promise.all([loadSequences(), loadCatalog(), loadSite()]).then(
201
+ ([sequences, catalog, site]) => {
202
+ if (site.accent) document.body.style.setProperty("--accent", site.accent);
203
+ if (site.accentAlt)
204
+ document.body.style.setProperty("--accent-alt", site.accentAlt);
205
+
206
+ const bookTitle = (id) => {
207
+ const b = catalog.books.find((x) => x.id === id);
208
+ return b ? b.title : id;
209
+ };
210
+
211
+ let audienceFilter = "all";
212
+
213
+ const stepLinks = (step) =>
214
+ (step.links || [])
215
+ .filter(
216
+ (l) => audienceFilter === "all" || l.audience === audienceFilter,
217
+ )
218
+ .map((l) => {
219
+ const alt = l.audience === "builders";
220
+ const label =
221
+ l.label ||
222
+ bookTitle(l.bookId) + " — " + roleLabel(catalog, l.audience);
223
+ return `<a class="journey-link ${alt ? "alt" : ""}" href="${readHref(l.bookId, l.audience, (l.chapters || [])[0])}">${escapeHtml(label)} →</a>`;
224
+ })
225
+ .join("");
226
+
227
+ const render = () => {
228
+ const html = sequences
229
+ .map((seq) => {
230
+ const steps = (seq.steps || [])
231
+ .filter(
232
+ (s) =>
233
+ audienceFilter === "all" ||
234
+ !s.audience ||
235
+ s.audience === audienceFilter,
236
+ )
237
+ .map((s, i) => {
238
+ const links = stepLinks(s);
239
+ const cls = s.required === false ? "journey-step optional" : "journey-step";
240
+ return `<li class="${cls}">
241
+ <button type="button" class="journey-step-head" data-journey-toggle aria-expanded="false">
242
+ <span class="journey-num">${i + 1}</span>
243
+ <span class="journey-label">${escapeHtml(s.label)}</span>
244
+ <span class="journey-flag">${s.required === false ? "optional" : "required"}</span>
245
+ </button>
246
+ <div class="journey-panel" hidden>
247
+ ${s.summary ? `<p class="journey-summary">${escapeHtml(s.summary)}</p>` : ""}
248
+ <div class="journey-links">${links || `<span class="journey-nolinks">No ${escapeHtml(audienceFilter)} chapter for this step — switch the filter.</span>`}</div>
249
+ </div>
250
+ </li>`;
251
+ })
252
+ .join("");
253
+ return `<div class="journey-seq">
254
+ <h2 class="journey-title">${escapeHtml(seq.title)}</h2>
255
+ ${seq.lead ? `<p class="journey-lead">${escapeHtml(seq.lead)}</p>` : ""}
256
+ <ol class="journey-steps">${steps}</ol>
257
+ </div>`;
258
+ })
259
+ .join("");
260
+
261
+ const filters = ["all", "developers", "builders"]
262
+ .map(
263
+ (f) =>
264
+ `<button type="button" class="journey-filter ${f === audienceFilter ? "active" : ""} ${f === "builders" ? "alt" : ""}" data-journey-filter="${f}">${f === "all" ? "All" : escapeHtml(roleLabel(catalog, f))}</button>`,
265
+ )
266
+ .join("");
267
+
268
+ root.innerHTML = `<div class="journey-toolbar"><span class="journey-toolbar-label">Show steps for</span>${filters}</div>${html}`;
269
+
270
+ root.querySelectorAll("[data-journey-toggle]").forEach((btn) => {
271
+ btn.addEventListener("click", () => {
272
+ const panel = btn.parentElement.querySelector(".journey-panel");
273
+ const open = !panel.hidden;
274
+ panel.hidden = open;
275
+ btn.setAttribute("aria-expanded", String(!open));
276
+ btn.parentElement.classList.toggle("open", !open);
277
+ });
278
+ });
279
+ root.querySelectorAll("[data-journey-filter]").forEach((btn) => {
280
+ btn.addEventListener("click", () => {
281
+ audienceFilter = btn.getAttribute("data-journey-filter");
282
+ render();
283
+ });
284
+ });
285
+ };
286
+
287
+ render();
288
+ },
289
+ ).catch(() => {
290
+ root.innerHTML =
291
+ '<p class="journey-loading">No journey published for this library yet.</p>';
292
+ });
293
+ }
294
+
152
295
  function hydratePage(kind) {
153
296
  bindNavToggle();
154
297
  if (kind === "catalog") {
@@ -159,6 +302,312 @@ function hydratePage(kind) {
159
302
  const root = document.getElementById("use-cases-root");
160
303
  if (root) hydrateUseCases(root);
161
304
  }
305
+ if (kind === "journey") {
306
+ const root = document.getElementById("journey-root");
307
+ if (root) hydrateJourney(root);
308
+ }
309
+ document
310
+ .querySelectorAll('[data-hydrate="tutorials"]')
311
+ .forEach((section) => hydrateTutorials(section));
312
+ document
313
+ .querySelectorAll("[data-diagram-id]")
314
+ .forEach((el) => hydrateDiagram(el));
315
+ const searchRoot = document.getElementById("search-root");
316
+ if (searchRoot) hydrateSearch(searchRoot);
317
+ }
318
+
319
+ function hydrateSearch(root) {
320
+ root.innerHTML = `
321
+ <label class="search-label" for="docify-search">Search knowledge</label>
322
+ <input id="docify-search" type="search" class="search-input" placeholder="Concept, guide, diagram, API…" autocomplete="off" />
323
+ <div class="search-filters" role="group" aria-label="Filter by type">
324
+ <button type="button" class="journey-filter active" data-search-type="">All</button>
325
+ <button type="button" class="journey-filter" data-search-type="concept">Concepts</button>
326
+ <button type="button" class="journey-filter" data-search-type="guide">Guides</button>
327
+ <button type="button" class="journey-filter" data-search-type="scenario">Scenarios</button>
328
+ <button type="button" class="journey-filter" data-search-type="diagram">Diagrams</button>
329
+ <button type="button" class="journey-filter" data-search-type="use-case">Use cases</button>
330
+ </div>
331
+ <ul class="search-results" id="search-results" aria-live="polite"></ul>`;
332
+ let type = "";
333
+ let index = { records: [] };
334
+ loadJson("/SEARCH.json")
335
+ .catch(() => loadJson("/agent/SEARCH.json"))
336
+ .then((data) => {
337
+ index = data || { records: [] };
338
+ })
339
+ .catch(() => {});
340
+
341
+ const render = () => {
342
+ const q = (document.getElementById("docify-search").value || "").trim().toLowerCase();
343
+ let records = index.records || [];
344
+ if (type) records = records.filter((r) => r.type === type);
345
+ if (q) {
346
+ records = records.filter((r) =>
347
+ `${r.id} ${r.title} ${r.summary} ${(r.aliases || []).join(" ")} ${(r.tags || []).join(" ")}`
348
+ .toLowerCase()
349
+ .includes(q),
350
+ );
351
+ }
352
+ const ul = document.getElementById("search-results");
353
+ if (!records.length) {
354
+ ul.innerHTML = "<li class='search-empty'>No matches.</li>";
355
+ return;
356
+ }
357
+ ul.innerHTML = records
358
+ .slice(0, 40)
359
+ .map((r) => {
360
+ const href = r.webPath || ("/" + String(r.path || "").replace(/\.md$/, ""));
361
+ return `<li><a href="${escapeHtml(href)}"><span class="search-type">${escapeHtml(r.type)}</span> <strong>${escapeHtml(r.title)}</strong> — ${escapeHtml(r.summary)}</a></li>`;
362
+ })
363
+ .join("");
364
+ };
365
+
366
+ document.getElementById("docify-search").addEventListener("input", render);
367
+ root.querySelectorAll("[data-search-type]").forEach((btn) => {
368
+ btn.addEventListener("click", () => {
369
+ type = btn.getAttribute("data-search-type") || "";
370
+ root.querySelectorAll("[data-search-type]").forEach((b) => b.classList.toggle("active", b === btn));
371
+ render();
372
+ });
373
+ });
374
+ }
375
+
376
+ /**
377
+ * Accessible diagram: list + table views always; spatial SVG when nodes/edges exist.
378
+ * Never relies on color alone; keyboard-focusable nodes.
379
+ */
380
+ function hydrateDiagram(el) {
381
+ const id = el.getAttribute("data-diagram-id");
382
+ const load = () =>
383
+ loadJson("/knowledge.json")
384
+ .then((k) => (k.diagrams || []).find((d) => d.id === id))
385
+ .catch(() => null);
386
+
387
+ load().then((diagram) => {
388
+ if (!diagram) {
389
+ el.innerHTML = "<p>Diagram not found.</p>";
390
+ return;
391
+ }
392
+ let view = "diagram";
393
+ const render = () => {
394
+ const tabs = ["diagram", "list", "table", "steps"]
395
+ .map(
396
+ (v) =>
397
+ `<button type="button" class="journey-filter ${v === view ? "active" : ""}" data-diagram-view="${v}">${v}</button>`,
398
+ )
399
+ .join("");
400
+ let body = "";
401
+ if (view === "list" || view === "steps") {
402
+ body =
403
+ `<ol class="diagram-list">` +
404
+ (diagram.nodes || [])
405
+ .map(
406
+ (n) =>
407
+ `<li tabindex="0"><strong>${escapeHtml(n.label)}</strong>${n.description ? " — " + escapeHtml(n.description) : ""}${n.conceptId ? ` · concept <code>${escapeHtml(n.conceptId)}</code>` : ""}</li>`,
408
+ )
409
+ .join("") +
410
+ `</ol>`;
411
+ } else if (view === "table") {
412
+ body =
413
+ `<table class="diagram-table"><caption>${escapeHtml(diagram.title)} relationships</caption><thead><tr><th>From</th><th>To</th><th>Label</th><th>Meaning</th></tr></thead><tbody>` +
414
+ (diagram.edges || [])
415
+ .map(
416
+ (e) =>
417
+ `<tr><td>${escapeHtml(e.from)}</td><td>${escapeHtml(e.to)}</td><td>${escapeHtml(e.label || "")}</td><td>${escapeHtml(e.description || "")}</td></tr>`,
418
+ )
419
+ .join("") +
420
+ `</tbody></table>`;
421
+ } else {
422
+ const nodes = diagram.nodes || [];
423
+ const w = Math.max(320, nodes.length * 140);
424
+ const h = 180;
425
+ const circles = nodes
426
+ .map((n, i) => {
427
+ const x = 60 + i * 140;
428
+ const y = 90;
429
+ return `<g tabindex="0" role="img" aria-label="${escapeHtml(n.label)}. ${escapeHtml(n.description || "")}">
430
+ <circle cx="${x}" cy="${y}" r="28" fill="var(--surface, #fff)" stroke="currentColor" stroke-width="2" />
431
+ <text x="${x}" y="${y + 4}" text-anchor="middle" font-size="11">${escapeHtml(n.label).slice(0, 12)}</text>
432
+ </g>`;
433
+ })
434
+ .join("");
435
+ const lines = (diagram.edges || [])
436
+ .map((e) => {
437
+ const fi = nodes.findIndex((n) => n.id === e.from);
438
+ const ti = nodes.findIndex((n) => n.id === e.to);
439
+ if (fi < 0 || ti < 0) return "";
440
+ const x1 = 60 + fi * 140;
441
+ const x2 = 60 + ti * 140;
442
+ return `<line x1="${x1 + 28}" y1="90" x2="${x2 - 28}" y2="90" stroke="currentColor" stroke-width="2" marker-end="url(#arrow)" />`;
443
+ })
444
+ .join("");
445
+ body = `<div class="diagram-spatial" role="region" aria-label="${escapeHtml(diagram.title)}">
446
+ <svg viewBox="0 0 ${w} ${h}" width="100%" height="${h}" role="img" aria-describedby="diagram-alt-${escapeHtml(diagram.id)}">
447
+ <defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="currentColor"/></marker></defs>
448
+ ${lines}${circles}
449
+ </svg>
450
+ </div>`;
451
+ }
452
+ el.innerHTML = `
453
+ <section class="diagram-block">
454
+ <header>
455
+ <h2>${escapeHtml(diagram.title)}</h2>
456
+ <p>${escapeHtml(diagram.summary)}</p>
457
+ <p><strong>Why:</strong> ${escapeHtml(diagram.why)}</p>
458
+ <p><strong>How to read:</strong> ${escapeHtml(diagram.howToRead)}</p>
459
+ </header>
460
+ <div class="diagram-toolbar" role="toolbar" aria-label="Diagram views">${tabs}</div>
461
+ ${body}
462
+ <p id="diagram-alt-${escapeHtml(diagram.id)}" class="diagram-text-alt"><strong>Text alternative:</strong> ${escapeHtml(diagram.textAlternative)}</p>
463
+ ${(diagram.keyTakeaways || []).length ? `<ul>${diagram.keyTakeaways.map((t) => `<li>${escapeHtml(t)}</li>`).join("")}</ul>` : ""}
464
+ ${diagram.mermaid ? `<details><summary>Mermaid source</summary><pre><code>${escapeHtml(diagram.mermaid)}</code></pre></details>` : ""}
465
+ </section>`;
466
+ el.querySelectorAll("[data-diagram-view]").forEach((btn) => {
467
+ btn.addEventListener("click", () => {
468
+ view = btn.getAttribute("data-diagram-view");
469
+ render();
470
+ });
471
+ });
472
+ };
473
+ render();
474
+ });
475
+ }
476
+
477
+ function hydrateRelationshipExplorer(root, panel) {
478
+ if (!root || !panel) return;
479
+ let view = "list";
480
+ let focusId = new URLSearchParams(location.search).get("id") || "";
481
+ let knowledge = { relationships: [] };
482
+
483
+ const render = () => {
484
+ const rels = knowledge.relationships || [];
485
+ const filtered = focusId
486
+ ? rels.filter((r) => r.from === focusId || r.to === focusId)
487
+ : rels;
488
+ root.innerHTML =
489
+ '<label for="explore-focus">Focus id</label>' +
490
+ '<input id="explore-focus" type="search" class="search-input" value="' +
491
+ escapeHtml(focusId) +
492
+ '" placeholder="concept or guide id" />' +
493
+ '<p class="muted">' +
494
+ filtered.length +
495
+ " declared relationship(s)</p>";
496
+ const input = document.getElementById("explore-focus");
497
+ input.addEventListener("change", (e) => {
498
+ focusId = e.target.value.trim();
499
+ const u = new URL(location.href);
500
+ if (focusId) u.searchParams.set("id", focusId);
501
+ else u.searchParams.delete("id");
502
+ history.replaceState(null, "", u);
503
+ render();
504
+ });
505
+
506
+ if (view === "table") {
507
+ panel.innerHTML =
508
+ '<table class="diagram-table"><caption>Declared relationships</caption><thead><tr><th>Type</th><th>From</th><th>To</th><th>Label</th></tr></thead><tbody>' +
509
+ filtered
510
+ .map(
511
+ (r) =>
512
+ "<tr><td>" +
513
+ escapeHtml(String(r.type)) +
514
+ '</td><td><a href="/explore?id=' +
515
+ encodeURIComponent(r.from) +
516
+ '">' +
517
+ escapeHtml(r.from) +
518
+ '</a></td><td><a href="/explore?id=' +
519
+ encodeURIComponent(r.to) +
520
+ '">' +
521
+ escapeHtml(r.to) +
522
+ "</a></td><td>" +
523
+ escapeHtml(r.label || r.description || "") +
524
+ "</td></tr>",
525
+ )
526
+ .join("") +
527
+ "</tbody></table>";
528
+ } else if (view === "diagram") {
529
+ const nodes = Array.from(new Set(filtered.flatMap((r) => [r.from, r.to])));
530
+ panel.innerHTML =
531
+ '<div class="diagram-spatial" role="region" aria-label="Relationship map"><svg viewBox="0 0 ' +
532
+ Math.max(320, nodes.length * 120) +
533
+ ' 160" width="100%" height="160">' +
534
+ nodes
535
+ .map((id, i) => {
536
+ const x = 50 + i * 120;
537
+ return (
538
+ '<g tabindex="0" role="img" aria-label="' +
539
+ escapeHtml(id) +
540
+ '"><circle cx="' +
541
+ x +
542
+ '" cy="80" r="24" fill="var(--surface,#fff)" stroke="currentColor" stroke-width="2"/><text x="' +
543
+ x +
544
+ '" y="84" text-anchor="middle" font-size="10">' +
545
+ escapeHtml(id).slice(0, 10) +
546
+ "</text></g>"
547
+ );
548
+ })
549
+ .join("") +
550
+ filtered
551
+ .map((r) => {
552
+ const a = nodes.indexOf(r.from);
553
+ const b = nodes.indexOf(r.to);
554
+ if (a < 0 || b < 0) return "";
555
+ return (
556
+ '<line x1="' +
557
+ (50 + a * 120) +
558
+ '" y1="80" x2="' +
559
+ (50 + b * 120) +
560
+ '" y2="80" stroke="currentColor" stroke-width="2"/>'
561
+ );
562
+ })
563
+ .join("") +
564
+ '</svg><p class="diagram-text-alt">Text alternative: ' +
565
+ (filtered.map((r) => r.from + " " + r.type + " " + r.to).join("; ") ||
566
+ "No relationships") +
567
+ ".</p></div>";
568
+ } else {
569
+ panel.innerHTML =
570
+ '<ul class="search-results">' +
571
+ (filtered
572
+ .map(
573
+ (r) =>
574
+ "<li><code>" +
575
+ escapeHtml(String(r.type)) +
576
+ '</code> <a href="/explore?id=' +
577
+ encodeURIComponent(r.from) +
578
+ '">' +
579
+ escapeHtml(r.from) +
580
+ '</a> → <a href="/explore?id=' +
581
+ encodeURIComponent(r.to) +
582
+ '">' +
583
+ escapeHtml(r.to) +
584
+ "</a>" +
585
+ (r.description ? " — " + escapeHtml(r.description) : "") +
586
+ "</li>",
587
+ )
588
+ .join("") || "<li>No declared relationships.</li>") +
589
+ "</ul>";
590
+ }
591
+ };
592
+
593
+ document.querySelectorAll("[data-explore-view]").forEach((btn) => {
594
+ btn.addEventListener("click", () => {
595
+ view = btn.getAttribute("data-explore-view") || "list";
596
+ document
597
+ .querySelectorAll("[data-explore-view]")
598
+ .forEach((b) => b.classList.toggle("active", b === btn));
599
+ render();
600
+ });
601
+ });
602
+
603
+ loadJson("/knowledge.json")
604
+ .then((k) => {
605
+ knowledge = k || knowledge;
606
+ render();
607
+ })
608
+ .catch(() => {
609
+ panel.innerHTML = "<p>No knowledge.json published yet.</p>";
610
+ });
162
611
  }
163
612
 
164
613
  bindNavToggle();