Feasibly Use the Web

A highly performant, beautiful, and dynamic heading navigation menu with virtualization.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Feasibly Use the Web
// @namespace    http://github.com/Echoinbyte/
// @version      3.0
// @description  A highly performant, beautiful, and dynamic heading navigation menu with virtualization.
// @author       Echoinbyte
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // --- Configuration ---
  const CONFIG = {
    debounceDelay: 750,
    virtualization: {
      itemHeight: 38,
      buffer: 8,
    },
    colors: {
      menuBackground: "rgba(30, 41, 59, 0.85)",
      menuBorder: "#475569",
      menuText: "#e2e8f0",
      menuTextSecondary: "#94a3b8",
      menuHover: "rgba(51, 65, 85, 0.9)",
      menuActive: "#1e293b",
      focusOutline: "#60a5fa",
      shadow: "rgba(0, 0, 0, 0.3)",
      accent: "#60a5fa",
      scrollbar: "#475569",
      scrollbarHover: "#64748b",
    },
  };

  // --- Core Logic: Manages state, data, and page observation ---
  const CoreLogic = {
    headings: [],
    filteredHeadings: [],
    mutationObserver: null,
    scrollObserver: null,
    updateTimeout: null,
    currentUrl: window.location.href,

    init() {
      if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", () => this.run());
      } else {
        this.run();
      }
    },

    run() {
      UIManager.init(this);
      this.discoverAndUpdateHeadings();
      this.setupObservers();
      this.setupEventListeners();
    },

    discoverAndUpdateHeadings(force = false) {
      const newHeadings = this.collectHeadings();
      const hasChanged =
        force ||
        this.headings.length !== newHeadings.length ||
        JSON.stringify(this.headings.map((h) => h.id)) !==
          JSON.stringify(newHeadings.map((h) => h.id));

      if (hasChanged) {
        this.headings = newHeadings;
        this.filteredHeadings = newHeadings;
        UIManager.render(this.filteredHeadings);
        this.observeVisibleHeadings();
      }
    },

    collectHeadings() {
      const headingNodes = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
      const headings = [];
      const counters = [0, 0, 0, 0, 0, 0];

      headingNodes.forEach((node, index) => {
        if (!node.textContent.trim() || node.closest("#feasible-heading-nav")) {
          return;
        }

        // TODO: Add support for custom heading selectors
        if (
          !node.id ||
          document.querySelector(
            `[id="${node.id}"]:not([data-heading-processed])`
          ) !== node
        ) {
          const baseId =
            "feasible-h-" +
            (node.textContent
              .trim()
              .toLowerCase()
              .replace(/[^a-z0-9\s]/g, "")
              .replace(/\s+/g, "-")
              .substring(0, 50) || index);

          let finalId = baseId;
          let counter = 2;
          while (
            document.getElementById(finalId) &&
            document.getElementById(finalId) !== node
          ) {
            finalId = `${baseId}-${counter++}`;
          }
          node.id = finalId;
        }

        node.setAttribute("data-heading-processed", "true");

        const level = parseInt(node.tagName.substring(1));

        // TODO: Add support for nested numbering schemes
        for (let i = 0; i < level - 1; i++) {
          if (counters[i] === 0) {
            counters[i] = 1;
          }
        }

        counters[level - 1]++;

        for (let i = level; i < 6; i++) {
          counters[i] = 0;
        }

        const numberLabel = counters
          .slice(0, level)
          .filter((c) => c > 0)
          .join(".");

        headings.push({
          id: node.id,
          text: node.textContent.trim(),
          level: level,
          number: numberLabel,
          element: node,
        });
      });
      return headings;
    },

    setupObservers() {
      this.mutationObserver = new MutationObserver(() => this.scheduleUpdate());
      this.mutationObserver.observe(document.body, {
        childList: true,
        subtree: true,
      });
    },

    observeVisibleHeadings() {
      if (this.scrollObserver) this.scrollObserver.disconnect();
      if (this.headings.length === 0) return;

      this.scrollObserver = new IntersectionObserver(
        (entries) => {
          // Find the most relevant heading to highlight
          let topMostEntry = null;
          let topMostPosition = Infinity;

          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              const rect = entry.boundingClientRect;
              // Prioritize headings that are closer to the top of the viewport
              if (rect.top < topMostPosition && rect.top >= 0) {
                topMostPosition = rect.top;
                topMostEntry = entry;
              }
            }
          });

          // Only update the active link if we found a valid entry
          if (topMostEntry) {
            UIManager.updateActiveLink(topMostEntry.target.id);
          }
        },
        { rootMargin: "0px 0px -80% 0px", threshold: 0.1 }
      );

      this.headings.forEach((h) => this.scrollObserver.observe(h.element));
    },

    scheduleUpdate() {
      clearTimeout(this.updateTimeout);
      this.updateTimeout = setTimeout(() => {
        if (window.location.href !== this.currentUrl) {
          this.currentUrl = window.location.href;
          this.discoverAndUpdateHeadings(true); // Force update on URL change
        } else {
          this.discoverAndUpdateHeadings();
        }
      }, CONFIG.debounceDelay);
    },

    setupEventListeners() {
      const schedule = () => this.scheduleUpdate();
      window.addEventListener("popstate", schedule);
      window.addEventListener("hashchange", schedule);

      // TODO: Add support for custom keyboard shortcuts
      document.addEventListener("keydown", (e) => {
        if (e.altKey && e.key.toLowerCase() === "h") {
          e.preventDefault();
          if (UIManager.elements.nav) {
            if (
              !UIManager.isCollapsed &&
              UIManager.core.filteredHeadings.length > 0
            ) {
              UIManager.elements.list.focus();
              UIManager.virtualScroll.focusedIndex = 0;
              UIManager.ensureIndexIsVisible(0);
              UIManager.updateVirtualScroll();
            } else {
              UIManager.elements.nav.focus();
            }
          }
        }

        if (e.altKey && e.key.toLowerCase() === "n") {
          e.preventDefault();
          if (UIManager.elements.filterInput && !UIManager.isCollapsed) {
            UIManager.elements.filterInput.focus();
          }
        }

        if (e.altKey && e.key.toLowerCase() === "t") {
          e.preventDefault();
          if (UIManager.elements.toggleBtn) {
            UIManager.elements.toggleBtn.click();
          }
        }
      });

      const originalPushState = history.pushState;
      history.pushState = function (...args) {
        originalPushState.apply(history, args);
        schedule();
      };
      const originalReplaceState = history.replaceState;
      history.replaceState = function (...args) {
        originalReplaceState.apply(history, args);
        schedule();
      };
    },

    filterHeadings(query) {
      const lowerQuery = query.toLowerCase();
      this.filteredHeadings = this.headings.filter((h) =>
        h.text.toLowerCase().includes(lowerQuery)
      );
      UIManager.render(this.filteredHeadings);
    },
  };

  // --- UI Manager: Manages all DOM elements and interactions ---
  const UIManager = {
    core: null,
    elements: {},
    isCollapsed: false,
    dragState: { isDragging: false, x: 0, y: 0, initialX: 0, initialY: 0 },
    virtualScroll: { scrollTop: 0, focusedIndex: -1 },

    init(coreInstance) {
      this.core = coreInstance;
      this.isCollapsed =
        localStorage.getItem("feasible-nav-collapsed") === "true";
      this.createStyles();
      this.createContainer();
      this.setupCoreEventListeners();
      this.applyInitialState();
      this.applyPersistedState();
    },

    render(headings) {
      this.virtualScroll.itemCount = headings.length;
      this.elements.listSizer.style.height = `${
        headings.length * CONFIG.virtualization.itemHeight
      }px`;
      this.updateVirtualScroll();
    },

    getStyleSheet(colors) {
      return `
        #feasible-heading-nav {
          position: fixed !important; top: 20px; right: 20px; width: 320px; max-height: 85vh;
          background: ${colors.menuBackground}; border: 1px solid ${colors.menuBorder};
          border-radius: 12px; box-shadow: 0 8px 32px ${colors.shadow};
          z-index: 999999 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          overflow: hidden; transition: width 0.3s ease, min-width 0.3s ease; backdrop-filter: blur(10px);
          user-select: none; pointer-events: auto !important; display: flex; flex-direction: column;
          color: ${colors.menuText};
        }
        .feasible-header {
          padding: 12px 16px; border-bottom: 1px solid ${colors.menuBorder}; display: flex;
          justify-content: space-between; align-items: center; cursor: grab; flex-shrink: 0;
        }
        .feasible-title-container { display: flex; align-items: center; gap: 8px; overflow: hidden; }
        .feasible-title-icon { font-size: 20px; user-select: none; }
        .feasible-title { margin: 0; font-size: 16px; font-weight: 600; color: ${colors.menuText}; white-space: nowrap; }
        .feasible-header button {
          color: ${colors.menuTextSecondary};
          display: flex;
          align-items: center;
          justify-content: center;
          width: 32px;
          height: 32px;
          line-height: 1;
        }
        .feasible-filter-input {
            width: calc(100% - 32px); padding: 8px 12px; margin: 8px 16px; border-radius: 6px; border: 1px solid ${colors.menuBorder};
            background-color: ${colors.menuBackground}; color: ${colors.menuText}; font-size: 13px; transition: border-color 0.2s ease;
            box-sizing: border-box; flex-shrink: 0;
        }
        .feasible-filter-input:focus { border-color: ${colors.accent}; outline: none; }
        .feasible-content { flex-grow: 1; overflow-y: auto; scroll-behavior: smooth; position: relative; }
        .feasible-content::-webkit-scrollbar { width: 8px; }
        .feasible-content::-webkit-scrollbar-track { background: transparent; }
        .feasible-content::-webkit-scrollbar-thumb { background: ${colors.scrollbar}; border-radius: 4px; }
        .feasible-content::-webkit-scrollbar-thumb:hover { background: ${colors.scrollbarHover}; }
        .feasible-list { list-style: none; margin: 0; padding: 0; position: relative; }
        .feasible-list-item {
          position: absolute; top: 0; left: 0; width: 100%; height: ${CONFIG.virtualization.itemHeight}px;
          display: flex; align-items: center; padding: 0 16px;
          color: ${colors.menuText}; text-decoration: none;
          transition: background-color 0.2s ease, border 0.2s ease; border: 1px solid transparent;
          cursor: pointer; box-sizing: border-box;
        }
        .feasible-list-item.active { background-color: ${colors.menuActive}; font-weight: 600; }
        .feasible-list-item.focused { border-color: ${colors.focusOutline}; }
        .feasible-list-item:hover { background-color: ${colors.menuHover}; }
        .item-number {
          font-size: 11px; font-weight: bold; text-align: center; line-height: 18px;
          border-radius: 4px; margin-right: 10px; color: white; padding: 0 6px;
        }
        .item-text { flex: 1; min-width: 0; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; font-size: 13px; }
      `;
    },

    createStyles() {
      this.elements.style = document.createElement("style");
      this.elements.style.textContent = this.getStyleSheet(CONFIG.colors);
      document.head.appendChild(this.elements.style);
    },

    createContainer() {
      const nav = document.createElement("nav");
      nav.id = "feasible-heading-nav";
      nav.setAttribute("tabindex", "1"); // Make it first in tab order
      nav.setAttribute("role", "navigation");
      nav.setAttribute("aria-label", "Page headings navigation");
      this.elements.nav = nav;

      const header = this.createHeader();
      const filter = this.createFilterInput();
      const content = this.createContentArea();

      nav.appendChild(header);
      nav.appendChild(filter);
      nav.appendChild(content);
      document.body.appendChild(nav);
    },

    createHeader() {
      const header = document.createElement("div");
      header.className = "feasible-header";
      this.elements.header = header;

      const titleContainer = document.createElement("div");
      titleContainer.className = "feasible-title-container";
      titleContainer.innerHTML = `<span class="feasible-title-icon">🧭</span><h2 class="feasible-title">Feasibley Navigate</h2>`;
      this.elements.titleContainer = titleContainer;

      const toggleBtn = document.createElement("button");
      toggleBtn.setAttribute("aria-label", "Collapse navigation");
      toggleBtn.setAttribute("tabindex", "2");
      toggleBtn.setAttribute("title", "Toggle navigation (Alt+T)");
      toggleBtn.style.cssText = `background: transparent; border: none; font-size: 24px; cursor: pointer; padding: 0 4px;`;
      this.elements.toggleBtn = toggleBtn;

      header.appendChild(titleContainer);
      header.appendChild(toggleBtn);
      return header;
    },

    createFilterInput() {
      const filterInput = document.createElement("input");
      filterInput.type = "text";
      filterInput.placeholder = "Navigate to...";
      filterInput.className = "feasible-filter-input";
      filterInput.setAttribute("tabindex", "3");
      filterInput.setAttribute("aria-label", "Filter headings");
      filterInput.setAttribute("title", "Search headings (Alt+N)");
      this.elements.filterInput = filterInput;
      return filterInput;
    },

    createContentArea() {
      const content = document.createElement("div");
      content.className = "feasible-content";
      this.elements.content = content;

      const listSizer = document.createElement("div");
      listSizer.style.cssText =
        "position: relative; width: 100%; height: 0; z-index: 0;";
      this.elements.listSizer = listSizer;

      const list = document.createElement("ul");
      list.className = "feasible-list";
      list.setAttribute("role", "menu");
      list.setAttribute("tabindex", "4");
      list.setAttribute("aria-label", "Headings list");
      list.setAttribute("title", "Navigate headings (Alt+H)");
      this.elements.list = list;

      listSizer.appendChild(list);
      content.appendChild(listSizer);
      return content;
    },

    updateVirtualScroll() {
      const { content, list } = this.elements;
      const { itemHeight, buffer } = CONFIG.virtualization;
      const itemCount = this.core.filteredHeadings.length;

      const startIndex = Math.max(
        0,
        Math.floor(this.virtualScroll.scrollTop / itemHeight) - buffer
      );
      const endIndex = Math.min(
        itemCount,
        Math.ceil(
          (this.virtualScroll.scrollTop + content.clientHeight) / itemHeight
        ) + buffer
      );

      const visibleItems = this.core.filteredHeadings.slice(
        startIndex,
        endIndex
      );

      list.innerHTML = ""; // Clear for simplicity, advanced recycling is more complex

      visibleItems.forEach((heading, i) => {
        const index = startIndex + i;
        const top = index * itemHeight;
        const li = this.createListItem(heading);
        li.style.transform = `translateY(${top}px)`;

        if (index === this.virtualScroll.focusedIndex) {
          li.classList.add("focused");
          li.setAttribute("aria-selected", "true");
        } else {
          li.setAttribute("aria-selected", "false");
        }

        list.appendChild(li);
      });
    },

    createListItem(heading) {
      const li = document.createElement("li");
      li.className = "feasible-list-item";
      li.dataset.id = heading.id;
      li.setAttribute("role", "menuitem");
      li.setAttribute("tabindex", "-1");
      li.setAttribute("title", `${heading.text} (Level ${heading.level})`);

      const level = heading.level;
      const indent = (level - 1) * 15;
      li.style.paddingLeft = `${16 + indent}px`;

      const colors = [
        "#3182ce",
        "#38a169",
        "#d69e2e",
        "#e53e3e",
        "#805ad5",
        "#dd6b20",
      ];
      const color = colors[level - 1] || colors[5];

      li.innerHTML = `
            <span class="item-number" style="background-color: ${color};">${heading.number}</span>
            <span class="item-text">${heading.text}</span>
        `;
      return li;
    },

    setupCoreEventListeners() {
      // Dragging
      this.elements.header.addEventListener("mousedown", (e) => {
        if (e.target.tagName === "BUTTON") return;
        e.preventDefault();
        this.dragState.isDragging = true;
        this.dragState.initialX = e.clientX - this.dragState.x;
        this.dragState.initialY = e.clientY - this.dragState.y;
        this.elements.header.style.cursor = "grabbing";
        document.body.style.userSelect = "none";
      });
      document.addEventListener("mousemove", (e) => {
        if (!this.dragState.isDragging) return;
        this.dragState.x = e.clientX - this.dragState.initialX;
        this.dragState.y = e.clientY - this.dragState.initialY;
        this.elements.nav.style.transform = `translate(${this.dragState.x}px, ${this.dragState.y}px)`;
      });
      document.addEventListener("mouseup", () => {
        if (this.dragState.isDragging) {
          this.dragState.isDragging = false;
          this.elements.header.style.cursor = "grab";
          document.body.style.userSelect = "";
          localStorage.setItem(
            "feasible-nav-position",
            JSON.stringify({ x: this.dragState.x, y: this.dragState.y })
          );
        }
      });

      // Collapse
      this.elements.toggleBtn.addEventListener("click", () => {
        this.isCollapsed = !this.isCollapsed;
        localStorage.setItem("feasible-nav-collapsed", this.isCollapsed);
        this.applyCollapseState();
      });

      // Main navigation keyboard controls
      this.elements.nav.addEventListener("keydown", (e) => {
        const { key } = e;

        // Handle Escape key to focus the navigation
        if (key === "Escape") {
          e.preventDefault();
          this.elements.nav.focus();
          return;
        }

        // Handle arrow keys to navigate to list when on nav
        if (key === "ArrowDown" && this.core.filteredHeadings.length > 0) {
          e.preventDefault();
          this.elements.list.focus();
          if (this.virtualScroll.focusedIndex === -1) {
            this.virtualScroll.focusedIndex = 0;
            this.ensureIndexIsVisible(0);
            this.updateVirtualScroll();
          }
          return;
        }
      });

      // Toggle button keyboard controls
      this.elements.toggleBtn.addEventListener("keydown", (e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          this.elements.toggleBtn.click();
        }
      });

      // TODO: Add support for custom filter shortcuts
      this.elements.filterInput.addEventListener("keydown", (e) => {
        if (e.key === "Tab" && !e.shiftKey) {
          return;
        }

        if (e.key === "ArrowDown") {
          e.preventDefault();
          this.elements.list.focus();
          if (
            this.virtualScroll.focusedIndex === -1 &&
            this.core.filteredHeadings.length > 0
          ) {
            this.virtualScroll.focusedIndex = 0;
            this.ensureIndexIsVisible(0);
            this.updateVirtualScroll();
          }
        }

        if (e.key === "Enter") {
          e.preventDefault();
          if (this.core.filteredHeadings.length > 0) {
            const firstHeading = this.core.filteredHeadings[0];
            const targetElement = document.getElementById(firstHeading.id);
            if (targetElement) {
              this.updateActiveLink(firstHeading.id);
              this.scrollToAndHighlight(targetElement);
            }
          }
        }

        if (e.key === "Escape") {
          e.preventDefault();
          this.elements.filterInput.value = "";
          this.core.filterHeadings("");
          this.elements.nav.focus();
        }
      });

      // Virtual Scroll
      this.elements.content.addEventListener(
        "scroll",
        (e) => {
          this.virtualScroll.scrollTop = e.target.scrollTop;
          requestAnimationFrame(() => this.updateVirtualScroll());
        },
        { passive: true }
      );

      // Filter
      this.elements.filterInput.addEventListener("input", (e) => {
        this.core.filterHeadings(e.target.value);
      });

      // Keyboard navigation for headings list
      this.elements.list.addEventListener("keydown", (e) => {
        const { key } = e;
        const allowedKeys = [
          "ArrowUp",
          "ArrowDown",
          "Enter",
          " ",
          "Home",
          "End",
          "PageUp",
          "PageDown",
          "Escape",
          "Tab",
        ];

        if (!allowedKeys.includes(key)) return;

        e.preventDefault();
        const count = this.core.filteredHeadings.length;
        if (count === 0) return;

        let { focusedIndex } = this.virtualScroll;

        // Initialize focused index if not set
        if (focusedIndex === -1) {
          focusedIndex = 0;
        }

        switch (key) {
          case "ArrowDown":
            focusedIndex = (focusedIndex + 1) % count;
            break;
          case "ArrowUp":
            focusedIndex = (focusedIndex - 1 + count) % count;
            break;
          case "Home":
            focusedIndex = 0;
            break;
          case "End":
            focusedIndex = count - 1;
            break;
          case "PageDown":
            focusedIndex = Math.min(count - 1, focusedIndex + 5);
            break;
          case "PageUp":
            focusedIndex = Math.max(0, focusedIndex - 5);
            break;
          case "Enter":
          case " ":
            if (focusedIndex !== -1) {
              const heading = this.core.filteredHeadings[focusedIndex];
              const targetElement = document.getElementById(heading.id);
              if (targetElement) {
                this.updateActiveLink(heading.id);
                this.scrollToAndHighlight(targetElement);
              }
            }
            return;
          case "Escape":
            this.elements.nav.focus();
            return;
          case "Tab":
            if (e.shiftKey) {
              this.elements.filterInput.focus();
            } else {
              // Allow tab to leave the navigation
              this.elements.nav.blur();
            }
            return;
        }

        this.virtualScroll.focusedIndex = focusedIndex;
        this.ensureIndexIsVisible(focusedIndex);
        this.updateVirtualScroll();
      });

      // Event Delegation for list items
      this.elements.list.addEventListener("click", (e) => {
        const item = e.target.closest(".feasible-list-item");
        if (!item) return;
        const targetId = item.dataset.id;
        const targetElement = document.getElementById(targetId);
        if (targetElement) {
          // Immediately update the active link to provide instant feedback
          this.updateActiveLink(targetId);
          this.scrollToAndHighlight(targetElement);
        }
      });
    },

    scrollToAndHighlight(element) {
      const originalStyles = {
        textDecoration: element.style.textDecoration,
        textDecorationColor: element.style.textDecorationColor,
        transition: element.style.transition,
      };

      element.style.transition = "text-decoration-color 2s ease-out";
      element.style.textDecoration = `underline solid ${CONFIG.colors.accent} 2px`;

      setTimeout(() => {
        element.style.textDecorationColor = "transparent";
      }, 200);

      setTimeout(() => {
        element.style.textDecoration = originalStyles.textDecoration;
        element.style.textDecorationColor = originalStyles.textDecorationColor;
        element.style.transition = originalStyles.transition;
      }, 2200);

      element.scrollIntoView({ behavior: "auto", block: "start" });

      const observer = new IntersectionObserver(
        (entries) => {
          observer.disconnect();
          const entry = entries[0];
          if (entry.isIntersecting && entry.boundingClientRect.top < 120) {
            const offset = 140 - entry.boundingClientRect.top;
            window.scrollBy({ top: -offset, behavior: "smooth" });
          }
        },
        { rootMargin: "0px 0px -90% 0px" }
      );

      observer.observe(element);
    },

    ensureIndexIsVisible(index) {
      const { content } = this.elements;
      const { itemHeight } = CONFIG.virtualization;
      const scrollTop = this.virtualScroll.scrollTop;
      const listHeight = content.clientHeight;

      const itemTop = index * itemHeight;
      const itemBottom = itemTop + itemHeight;

      if (itemTop < scrollTop) {
        content.scrollTop = itemTop;
      } else if (itemBottom > scrollTop + listHeight) {
        content.scrollTop = itemBottom - listHeight;
      }
    },

    applyPersistedState() {
      const savedPosition = localStorage.getItem("feasible-nav-position");
      if (savedPosition) {
        try {
          const { x, y } = JSON.parse(savedPosition);
          if (typeof x === "number" && typeof y === "number") {
            this.dragState.x = x;
            this.dragState.y = y;
            this.elements.nav.style.transform = `translate(${x}px, ${y}px)`;
          }
        } catch (e) {
          console.error("FeasibleNav: Could not parse saved position.", e);
          localStorage.removeItem("feasible-nav-position");
        }
      }
    },

    applyInitialState() {
      this.applyCollapseState();
    },

    applyCollapseState() {
      const { nav, titleContainer, content, toggleBtn, filterInput } =
        this.elements;
      if (this.isCollapsed) {
        titleContainer.style.display = "none";
        content.style.display = "none";
        filterInput.style.display = "none";
        toggleBtn.innerHTML = "+";
        nav.style.width = "auto";
        nav.style.minWidth = "48px";
      } else {
        titleContainer.style.display = "flex";
        content.style.display = "block";
        filterInput.style.display = "block";
        toggleBtn.innerHTML = "−";
        nav.style.width = "320px";
      }
    },

    updateActiveLink(id) {
      this.elements.list
        .querySelectorAll(".feasible-list-item")
        .forEach((item) => {
          item.classList.toggle("active", item.dataset.id === id);
        });
    },
  };

  CoreLogic.init();
})();