Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T00:29:46.148Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

plugins.js

385 lines · 10.7 KB · javascript
1function detectOS() {
2  var userAgent = navigator.userAgent;
3
4  var platform = navigator.platform;
5  var macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"];
6  var windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
7  var iosPlatforms = ["iPhone", "iPad", "iPod"];
8
9  if (macosPlatforms.indexOf(platform) !== -1) {
10    return "Mac";
11  } else if (iosPlatforms.indexOf(platform) !== -1) {
12    return "iOS";
13  } else if (windowsPlatforms.indexOf(platform) !== -1) {
14    return "Windows";
15  } else if (/Android/.test(userAgent)) {
16    return "Android";
17  } else if (/Linux/.test(platform)) {
18    return "Linux";
19  }
20
21  return "Unknown";
22}
23
24var os = detectOS();
25console.log("Operating System:", os);
26
27// Defer keybinding processing to avoid blocking initial render
28function updateKeybindings() {
29  const os = detectOS();
30  const isMac = os === "Mac" || os === "iOS";
31
32  function processKeybinding(element) {
33    const [macKeybinding, linuxKeybinding] = element.textContent.split("|");
34    element.textContent = isMac ? macKeybinding : linuxKeybinding;
35    element.classList.add("keybinding");
36  }
37
38  // Process all kbd elements at once (more efficient than walking entire DOM)
39  const kbdElements = document.querySelectorAll("kbd");
40  kbdElements.forEach(processKeybinding);
41}
42
43// Use requestIdleCallback if available, otherwise requestAnimationFrame
44if (typeof requestIdleCallback === "function") {
45  requestIdleCallback(updateKeybindings);
46} else {
47  requestAnimationFrame(updateKeybindings);
48}
49
50function darkModeToggle() {
51  var html = document.documentElement;
52
53  function setTheme(theme) {
54    html.setAttribute("data-theme", theme);
55    html.setAttribute("data-color-scheme", theme);
56    html.className = theme;
57    localStorage.setItem("mdbook-theme", theme);
58  }
59
60  // Set initial theme
61  var currentTheme = localStorage.getItem("mdbook-theme");
62  if (currentTheme) {
63    setTheme(currentTheme);
64  } else {
65    // If no theme is set, use the system's preference
66    var systemPreference = window.matchMedia("(prefers-color-scheme: dark)")
67      .matches
68      ? "dark"
69      : "light";
70    setTheme(systemPreference);
71  }
72
73  // Listen for system's preference changes
74  const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
75  darkModeMediaQuery.addEventListener("change", function (e) {
76    if (!localStorage.getItem("mdbook-theme")) {
77      setTheme(e.matches ? "dark" : "light");
78    }
79  });
80}
81
82const copyPageActions = () => {
83  const headerCopyMarkdownButton = document.getElementById(
84    "copy-markdown-toggle",
85  );
86
87  const actionButtons = new Map();
88  let isLoading = false;
89
90  const showToast = (message, isSuccess = true) => {
91    const existingToast = document.getElementById("copy-toast");
92    existingToast?.remove();
93
94    const toast = document.createElement("div");
95    toast.id = "copy-toast";
96    toast.className = `copy-toast ${isSuccess ? "success" : "error"}`;
97    toast.textContent = message;
98
99    document.body.appendChild(toast);
100
101    setTimeout(() => {
102      toast.classList.add("show");
103    }, 10);
104
105    setTimeout(() => {
106      toast.classList.remove("show");
107      setTimeout(() => {
108        toast.parentNode?.removeChild(toast);
109      }, 300);
110    }, 2000);
111  };
112
113  const registerButton = (button, originalIconClass) => {
114    if (!button) return;
115    actionButtons.set(button, {
116      originalIconClass,
117      iconTimeoutId: null,
118    });
119  };
120
121  registerButton(headerCopyMarkdownButton, "fa fa-copy");
122
123  const changeButtonIcon = (button, iconClass, duration = 1000) => {
124    const state = actionButtons.get(button);
125    if (!state) return;
126
127    const icon = button.querySelector("i");
128    if (!icon) return;
129
130    if (state.iconTimeoutId) {
131      clearTimeout(state.iconTimeoutId);
132      state.iconTimeoutId = null;
133    }
134
135    icon.className = iconClass;
136
137    if (duration > 0) {
138      state.iconTimeoutId = setTimeout(() => {
139        icon.className = state.originalIconClass;
140        state.iconTimeoutId = null;
141      }, duration);
142    }
143  };
144
145  const copyText = async (text) => {
146    if (!navigator.clipboard?.writeText) {
147      throw new Error("Clipboard API not supported in this browser");
148    }
149
150    await navigator.clipboard.writeText(text);
151  };
152
153  const getContentRoot = () =>
154    document.querySelector("#content main .content-wrap") ||
155    document.querySelector("#content > main");
156
157  const markdownAlternateHref = () =>
158    document
159      .querySelector('link[rel="alternate"][type="text/markdown"]')
160      ?.getAttribute("href");
161
162  const insertPageActionButtons = () => {
163    const contentRoot = getContentRoot();
164    if (
165      !contentRoot ||
166      contentRoot.querySelector(".page-actions") ||
167      !headerCopyMarkdownButton ||
168      !markdownAlternateHref()
169    ) {
170      return;
171    }
172
173    const firstHeading = contentRoot.querySelector("h1");
174    if (!firstHeading) return;
175
176    const header = document.createElement("div");
177    header.className = "page-header";
178
179    const actions = document.createElement("div");
180    actions.className = "page-actions";
181
182    firstHeading.insertAdjacentElement("beforebegin", header);
183    headerCopyMarkdownButton.classList.remove(
184      "icon-button",
185      "ib-hidden-mobile",
186    );
187    headerCopyMarkdownButton.classList.add("page-action", "page-action-icon");
188    actions.append(headerCopyMarkdownButton);
189    header.append(firstHeading, actions);
190  };
191
192  const markdownUrl = () => {
193    const alternateHref = markdownAlternateHref();
194    if (!alternateHref) return null;
195
196    const url = new URL(alternateHref, window.location.href);
197    if (
198      url.origin === window.location.origin &&
199      url.pathname.startsWith("/docs/") &&
200      !window.location.pathname.startsWith("/docs/")
201    ) {
202      return url.pathname.replace(/^\/docs/, "") || "/";
203    }
204
205    if (url.origin === window.location.origin) {
206      return `${url.pathname}${url.search}${url.hash}`;
207    }
208
209    return url.pathname;
210  };
211
212  const fetchAndCopyMarkdown = async (button) => {
213    // Prevent multiple simultaneous requests
214    if (isLoading) return;
215
216    try {
217      isLoading = true;
218      changeButtonIcon(button, "fa fa-spinner fa-spin", 0); // Don't auto-restore spinner
219
220      const url = markdownUrl();
221      if (!url) {
222        throw new Error("Markdown alternate link not found");
223      }
224
225      const response = await fetch(url);
226      if (!response.ok) {
227        throw new Error(
228          `Failed to fetch markdown: ${response.status} ${response.statusText}`,
229        );
230      }
231
232      const markdownContent = await response.text();
233
234      await copyText(markdownContent);
235
236      changeButtonIcon(button, "fa fa-check", 1000);
237      showToast("Markdown copied to clipboard!");
238    } catch (error) {
239      console.error("Error copying markdown:", error);
240      changeButtonIcon(button, "fa fa-exclamation-triangle", 2000);
241      showToast("Failed to copy markdown. Please try again.", false);
242    } finally {
243      isLoading = false;
244    }
245  };
246
247  headerCopyMarkdownButton?.addEventListener("click", () =>
248    fetchAndCopyMarkdown(headerCopyMarkdownButton),
249  );
250
251  insertPageActionButtons();
252};
253
254const initializePlugins = () => {
255  darkModeToggle();
256  requestAnimationFrame(copyPageActions);
257};
258
259if (document.readyState === "loading") {
260  document.addEventListener("DOMContentLoaded", initializePlugins);
261} else {
262  initializePlugins();
263}
264
265// Collapsible sidebar navigation for entire sections
266// Note: Initial collapsed state is applied in index.hbs to prevent flicker
267function initCollapsibleSidebar() {
268  var sidebar = document.getElementById("sidebar");
269  if (!sidebar) return;
270
271  var chapterList = sidebar.querySelector("ol.chapter");
272  if (!chapterList) return;
273
274  var partTitles = Array.from(chapterList.querySelectorAll("li.part-title"));
275
276  partTitles.forEach(function (partTitle) {
277    // Get all sibling elements that belong to this section
278    var sectionItems = getSectionItems(partTitle);
279
280    if (sectionItems.length > 0) {
281      setupCollapsibleSection(partTitle, sectionItems);
282    }
283  });
284}
285
286// Saves the list of collapsed section names to sessionStorage
287// This gets reset when the tab is closed and opened again
288function saveCollapsedSections() {
289  var collapsedSections = [];
290  var partTitles = document.querySelectorAll(
291    "#sidebar li.part-title.collapsible",
292  );
293
294  partTitles.forEach(function (partTitle) {
295    if (!partTitle.classList.contains("expanded")) {
296      collapsedSections.push(partTitle._sectionName);
297    }
298  });
299
300  try {
301    sessionStorage.setItem(
302      "sidebar-collapsed-sections",
303      JSON.stringify(collapsedSections),
304    );
305  } catch (e) {
306    // sessionStorage might not be available
307  }
308}
309
310function getSectionItems(partTitle) {
311  var items = [];
312  var sibling = partTitle.nextElementSibling;
313
314  while (sibling) {
315    // Stop when we hit another part-title
316    if (sibling.classList.contains("part-title")) {
317      break;
318    }
319    items.push(sibling);
320    sibling = sibling.nextElementSibling;
321  }
322
323  return items;
324}
325
326function setupCollapsibleSection(partTitle, sectionItems) {
327  partTitle.classList.add("collapsible");
328  partTitle.setAttribute("role", "button");
329  partTitle.setAttribute("tabindex", "0");
330  partTitle._sectionItems = sectionItems;
331
332  var isCurrentlyCollapsed = partTitle._isCollapsed;
333  if (isCurrentlyCollapsed) {
334    partTitle.setAttribute("aria-expanded", "false");
335  } else {
336    partTitle.classList.add("expanded");
337    partTitle.setAttribute("aria-expanded", "true");
338  }
339
340  partTitle.addEventListener("click", function (e) {
341    e.preventDefault();
342    toggleSection(partTitle);
343  });
344
345  // a11y: Add keyboard support (Enter and Space)
346  partTitle.addEventListener("keydown", function (e) {
347    if (e.key === "Enter" || e.key === " ") {
348      e.preventDefault();
349      toggleSection(partTitle);
350    }
351  });
352}
353
354function toggleSection(partTitle) {
355  var isExpanded = partTitle.classList.contains("expanded");
356  var sectionItems = partTitle._sectionItems;
357  var spacerAfter = partTitle._spacerAfter;
358
359  if (isExpanded) {
360    partTitle.classList.remove("expanded");
361    partTitle.setAttribute("aria-expanded", "false");
362    sectionItems.forEach(function (item) {
363      item.classList.add("section-hidden");
364    });
365    if (spacerAfter) {
366      spacerAfter.classList.add("section-hidden");
367    }
368  } else {
369    partTitle.classList.add("expanded");
370    partTitle.setAttribute("aria-expanded", "true");
371    sectionItems.forEach(function (item) {
372      item.classList.remove("section-hidden");
373    });
374    if (spacerAfter) {
375      spacerAfter.classList.remove("section-hidden");
376    }
377  }
378
379  saveCollapsedSections();
380}
381
382document.addEventListener("DOMContentLoaded", function () {
383  initCollapsibleSidebar();
384});
385
Served at tenant.openagents/omega Member data and write actions are omitted.