Feasibly Use the Web

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();