const y = '<svg viewBox="0 0 16 16" width="12" height="12"><path fill="currentColor" d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/></svg>', I = {
  expand: "▶",
  collapse: "▼",
  sortAsc: "▲",
  sortDesc: "▼",
  sortNone: "⇅",
  submenuArrow: "▶",
  dragHandle: "⋮⋮",
  toolPanel: "☰",
  filter: y,
  filterActive: y,
  print: "🖨️"
};
class M {
  /**
   * Plugin dependencies - declare other plugins this one requires.
   *
   * Dependencies are validated when the plugin is attached.
   * Required dependencies throw an error if missing.
   * Optional dependencies log an info message if missing.
   *
   * @example
   * ```typescript
   * static readonly dependencies: PluginDependency[] = [
   *   { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },
   *   { name: 'selection', required: false, reason: 'Enables selection-based undo' },
   * ];
   * ```
   */
  static dependencies;
  /**
   * Plugin manifest - declares owned properties, config rules, and hook priorities.
   *
   * This is read by the configuration validator to:
   * - Validate that required plugins are loaded when their properties are used
   * - Execute configRules to detect invalid/conflicting settings
   * - Order hook execution based on priority
   *
   * @example
   * ```typescript
   * static override readonly manifest: PluginManifest<MyConfig> = {
   *   ownedProperties: [
   *     { property: 'myProp', level: 'column', description: 'the "myProp" column property' },
   *   ],
   *   configRules: [
   *     { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },
   *   ],
   * };
   * ```
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  static manifest;
  /**
   * Plugin version - defaults to grid version for built-in plugins.
   * Third-party plugins can override with their own semver.
   */
  version = typeof __GRID_VERSION__ < "u" ? __GRID_VERSION__ : "dev";
  /** CSS styles to inject into the grid's shadow DOM */
  styles;
  /** Custom cell renderers keyed by type name */
  cellRenderers;
  /** Custom header renderers keyed by type name */
  headerRenderers;
  /** Custom cell editors keyed by type name */
  cellEditors;
  /** The grid instance this plugin is attached to */
  grid;
  /** Plugin configuration - merged with defaults in attach() */
  config;
  /** User-provided configuration from constructor */
  userConfig;
  /**
   * Plugin-level AbortController for event listener cleanup.
   * Created fresh in attach(), aborted in detach().
   * This ensures event listeners are properly cleaned up when plugins are re-attached.
   */
  #e;
  /**
   * Default configuration - subclasses should override this getter.
   * Note: This must be a getter (not property initializer) for proper inheritance
   * since property initializers run after parent constructor.
   */
  get defaultConfig() {
    return {};
  }
  constructor(e = {}) {
    this.userConfig = e;
  }
  /**
   * Called when the plugin is attached to a grid.
   * Override to set up event listeners, initialize state, etc.
   *
   * @example
   * ```ts
   * attach(grid: GridElement): void {
   *   super.attach(grid);
   *   // Set up document-level listeners with auto-cleanup
   *   document.addEventListener('keydown', this.handleEscape, {
   *     signal: this.disconnectSignal
   *   });
   * }
   * ```
   */
  attach(e) {
    this.#e?.abort(), this.#e = new AbortController(), this.grid = e, this.config = { ...this.defaultConfig, ...this.userConfig };
  }
  /**
   * Called when the plugin is detached from a grid.
   * Override to clean up event listeners, timers, etc.
   *
   * @example
   * ```ts
   * detach(): void {
   *   // Clean up any state not handled by disconnectSignal
   *   this.selectedRows.clear();
   *   this.cache = null;
   * }
   * ```
   */
  detach() {
    this.#e?.abort(), this.#e = void 0;
  }
  /**
   * Get another plugin instance from the same grid.
   * Use for inter-plugin communication.
   *
   * @example
   * ```ts
   * const selection = this.getPlugin(SelectionPlugin);
   * if (selection) {
   *   const selectedRows = selection.getSelectedRows();
   * }
   * ```
   */
  getPlugin(e) {
    return this.grid?.getPlugin(e);
  }
  /**
   * Emit a custom event from the grid.
   */
  emit(e, t) {
    this.grid?.dispatchEvent?.(new CustomEvent(e, { detail: t, bubbles: !0 }));
  }
  /**
   * Emit a cancelable custom event from the grid.
   * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise
   */
  emitCancelable(e, t) {
    const n = new CustomEvent(e, { detail: t, bubbles: !0, cancelable: !0 });
    return this.grid?.dispatchEvent?.(n), n.defaultPrevented;
  }
  /**
   * Request a re-render of the grid.
   */
  requestRender() {
    this.grid?.requestRender?.();
  }
  /**
   * Request a re-render and restore focus styling afterward.
   * Use this when a plugin action (like expand/collapse) triggers a render
   * but needs to maintain keyboard navigation focus.
   */
  requestRenderWithFocus() {
    this.grid?.requestRenderWithFocus?.();
  }
  /**
   * Request a lightweight style update without rebuilding DOM.
   * Use this instead of requestRender() when only CSS classes need updating.
   */
  requestAfterRender() {
    this.grid?.requestAfterRender?.();
  }
  /**
   * Get the current rows from the grid.
   */
  get rows() {
    return this.grid?.rows ?? [];
  }
  /**
   * Get the original unfiltered/unprocessed rows from the grid.
   * Use this when you need all source data regardless of active filters.
   */
  get sourceRows() {
    return this.grid?.sourceRows ?? [];
  }
  /**
   * Get the current columns from the grid.
   */
  get columns() {
    return this.grid?.columns ?? [];
  }
  /**
   * Get only visible columns from the grid (excludes hidden).
   * Use this for rendering that needs to match the grid template.
   */
  get visibleColumns() {
    return this.grid?._visibleColumns ?? [];
  }
  /**
   * Get the grid as an HTMLElement for direct DOM operations.
   * Use sparingly - prefer the typed GridElementRef API when possible.
   *
   * @example
   * ```ts
   * const width = this.gridElement.clientWidth;
   * this.gridElement.classList.add('my-plugin-active');
   * ```
   */
  get gridElement() {
    return this.grid;
  }
  /**
   * Get the disconnect signal for event listener cleanup.
   * This signal is aborted when the grid disconnects from the DOM.
   * Use this when adding event listeners that should be cleaned up automatically.
   *
   * Best for:
   * - Document/window-level listeners added in attach()
   * - Listeners on the grid element itself
   * - Any listener that should persist across renders
   *
   * Not needed for:
   * - Listeners on elements created in afterRender() (removed with element)
   *
   * @example
   * element.addEventListener('click', handler, { signal: this.disconnectSignal });
   * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });
   */
  get disconnectSignal() {
    return this.#e?.signal ?? this.grid?.disconnectSignal;
  }
  /**
   * Get the grid-level icons configuration.
   * Returns merged icons (user config + defaults).
   */
  get gridIcons() {
    const e = this.grid?.gridConfig?.icons ?? {};
    return { ...I, ...e };
  }
  // #region Animation Helpers
  /**
   * Check if animations are enabled at the grid level.
   * Respects gridConfig.animation.mode and the CSS variable set by the grid.
   *
   * Plugins should use this to skip animations when:
   * - Animation mode is 'off' or `false`
   * - User prefers reduced motion and mode is 'reduced-motion' (default)
   *
   * @example
   * ```ts
   * private get animationStyle(): 'slide' | 'fade' | false {
   *   if (!this.isAnimationEnabled) return false;
   *   return this.config.animation ?? 'slide';
   * }
   * ```
   */
  get isAnimationEnabled() {
    const e = this.grid?.effectiveConfig?.animation?.mode ?? "reduced-motion";
    if (e === !1 || e === "off") return !1;
    if (e === !0 || e === "on") return !0;
    const t = this.gridElement;
    return t ? getComputedStyle(t).getPropertyValue("--tbw-animation-enabled").trim() !== "0" : !0;
  }
  /**
   * Get the animation duration in milliseconds from CSS variable.
   * Falls back to 200ms if not set.
   *
   * Plugins can use this for their animation timing to stay consistent
   * with the grid-level animation.duration setting.
   *
   * @example
   * ```ts
   * element.animate(keyframes, { duration: this.animationDuration });
   * ```
   */
  get animationDuration() {
    const e = this.gridElement;
    if (e) {
      const t = getComputedStyle(e).getPropertyValue("--tbw-animation-duration").trim(), n = parseInt(t, 10);
      if (!isNaN(n)) return n;
    }
    return 200;
  }
  // #endregion
  /**
   * Resolve an icon value to string or HTMLElement.
   * Checks plugin config first, then grid-level icons, then defaults.
   *
   * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')
   * @param pluginOverride - Optional plugin-level override
   * @returns The resolved icon value
   */
  resolveIcon(e, t) {
    return t !== void 0 ? t : this.gridIcons[e];
  }
  /**
   * Set an icon value on an element.
   * Handles both string (text/HTML) and HTMLElement values.
   *
   * @param element - The element to set the icon on
   * @param icon - The icon value (string or HTMLElement)
   */
  setIcon(e, t) {
    typeof t == "string" ? e.innerHTML = t : t instanceof HTMLElement && (e.innerHTML = "", e.appendChild(t.cloneNode(!0)));
  }
  /**
   * Log a warning message.
   */
  warn(e) {
    console.warn(`[tbw-grid:${this.name}] ${e}`);
  }
  // #endregion
}
const w = "@layer tbw-plugins{.tbw-context-menu{position:fixed;background:var(--tbw-context-menu-bg, var(--tbw-color-panel-bg));color:var(--tbw-context-menu-fg, var(--tbw-color-fg));border:1px solid var(--tbw-context-menu-border, var(--tbw-color-border));border-radius:var(--tbw-context-menu-radius, var(--tbw-border-radius));box-shadow:var(--tbw-context-menu-shadow, 0 2px 10px var(--tbw-color-shadow));min-width:var(--tbw-context-menu-min-width, var(--tbw-menu-min-width));padding:var(--tbw-spacing-xs) 0;z-index:10000;font-size:var(--tbw-context-menu-font-size, var(--tbw-font-size-sm));font-family:var(--tbw-context-menu-font-family, var(--tbw-font-family))}.tbw-context-menu-item{display:flex;align-items:center;padding:var(--tbw-context-menu-item-padding, var(--tbw-menu-item-padding));cursor:pointer;gap:var(--tbw-context-menu-item-gap, var(--tbw-menu-item-gap))}.tbw-context-menu-item:hover:not(.disabled){background:var(--tbw-context-menu-hover, var(--tbw-color-row-hover))}.tbw-context-menu-item.disabled{opacity:.5;cursor:default}.tbw-context-menu-item.danger{color:var(--tbw-context-menu-danger, var(--tbw-color-danger))}.tbw-context-menu-icon{width:var(--tbw-context-menu-icon-size, var(--tbw-icon-size));text-align:center}.tbw-context-menu-label{flex:1}.tbw-context-menu-shortcut{color:var(--tbw-context-menu-muted, var(--tbw-color-fg-muted));font-size:var(--tbw-context-menu-shortcut-size, var(--tbw-font-size-xs))}.tbw-context-menu-arrow{font-size:var(--tbw-context-menu-arrow-size, var(--tbw-font-size-2xs));color:var(--tbw-context-menu-muted, var(--tbw-color-fg-muted))}.tbw-context-menu-separator{height:1px;background:var(--tbw-context-menu-border, var(--tbw-color-border));margin:var(--tbw-spacing-xs) 0}}";
function x(s, e) {
  return (typeof s == "function" ? s(e) : s).filter((n) => !(n.hidden === !0 || typeof n.hidden == "function" && n.hidden(e)));
}
function z(s, e) {
  return s.disabled === !0 ? !0 : typeof s.disabled == "function" ? s.disabled(e) : !1;
}
function v(s, e, t, n = I.submenuArrow) {
  const r = document.createElement("div");
  r.className = "tbw-context-menu", r.setAttribute("role", "menu");
  for (const i of s) {
    if (i.separator) {
      const l = document.createElement("div");
      l.className = "tbw-context-menu-separator", l.setAttribute("role", "separator"), r.appendChild(l);
      continue;
    }
    const o = document.createElement("div");
    o.className = "tbw-context-menu-item", i.cssClass && o.classList.add(i.cssClass), o.setAttribute("role", "menuitem"), o.setAttribute("data-id", i.id);
    const u = z(i, e);
    if (u && (o.classList.add("disabled"), o.setAttribute("aria-disabled", "true")), i.icon) {
      const l = document.createElement("span");
      l.className = "tbw-context-menu-icon", l.innerHTML = i.icon, o.appendChild(l);
    }
    const c = document.createElement("span");
    if (c.className = "tbw-context-menu-label", c.textContent = i.name, o.appendChild(c), i.shortcut) {
      const l = document.createElement("span");
      l.className = "tbw-context-menu-shortcut", l.textContent = i.shortcut, o.appendChild(l);
    }
    if (i.subMenu?.length) {
      const l = document.createElement("span");
      l.className = "tbw-context-menu-arrow", typeof n == "string" ? l.innerHTML = n : n instanceof HTMLElement && l.appendChild(n.cloneNode(!0)), o.appendChild(l), o.addEventListener("mouseenter", () => {
        if (o.querySelector(".tbw-context-menu") || !i.subMenu) return;
        const b = x(i.subMenu, e), d = v(b, e, t, n);
        d.classList.add("tbw-context-submenu"), d.style.position = "absolute", d.style.left = "100%", d.style.top = "0", o.style.position = "relative", o.appendChild(d);
      }), o.addEventListener("mouseleave", () => {
        const a = o.querySelector(".tbw-context-menu");
        a && a.remove();
      });
    }
    !u && i.action && !i.subMenu && o.addEventListener("click", (l) => {
      l.stopPropagation(), t(i);
    }), r.appendChild(o);
  }
  return r;
}
function C(s, e, t) {
  s.style.position = "fixed", s.style.left = `${e}px`, s.style.top = `${t}px`, s.style.visibility = "hidden", s.style.zIndex = "10000";
  const n = s.getBoundingClientRect(), r = window.innerWidth, i = window.innerHeight;
  let o = e, u = t;
  e + n.width > r && (o = e - n.width), t + n.height > i && (u = t - n.height), o = Math.max(0, o), u = Math.max(0, u), s.style.left = `${o}px`, s.style.top = `${u}px`, s.style.visibility = "visible";
}
let h = null, f = null, m = null, g = 0;
const p = [
  {
    id: "copy",
    name: "Copy",
    shortcut: "Ctrl+C",
    action: (s) => {
      s.grid?.plugins?.clipboard?.copy?.();
    }
  },
  { separator: !0, id: "sep1", name: "" },
  {
    id: "export-csv",
    name: "Export CSV",
    action: (s) => {
      s.grid?.plugins?.export?.exportCsv?.();
    }
  }
];
class S extends M {
  /** @internal */
  name = "contextMenu";
  /** @internal */
  get defaultConfig() {
    return {
      items: p
    };
  }
  // #region Internal State
  isOpen = !1;
  position = { x: 0, y: 0 };
  params = null;
  menuElement = null;
  // #endregion
  // #region Lifecycle
  /** @internal */
  attach(e) {
    super.attach(e), this.installGlobalHandlers(), g++;
  }
  /** @internal */
  detach() {
    this.menuElement && (this.menuElement.remove(), this.menuElement = null), this.isOpen = !1, this.params = null, this.uninstallGlobalHandlers();
  }
  // #endregion
  // #region Private Methods
  /**
   * CSS variables to copy from the grid element to the context menu.
   * Includes both base variables and context-menu specific overrides.
   */
  static CSS_VARS_TO_COPY = [
    // Base palette (for themes that only set base vars)
    "--tbw-color-panel-bg",
    "--tbw-color-fg",
    "--tbw-color-fg-muted",
    "--tbw-color-border",
    "--tbw-color-row-hover",
    "--tbw-color-shadow",
    "--tbw-color-danger",
    "--tbw-border-radius",
    "--tbw-font-family",
    "--tbw-font-size-sm",
    "--tbw-font-size-xs",
    "--tbw-font-size-2xs",
    "--tbw-spacing-xs",
    "--tbw-icon-size",
    "--tbw-menu-min-width",
    "--tbw-menu-item-padding",
    "--tbw-menu-item-gap",
    // Context menu specific overrides
    "--tbw-context-menu-bg",
    "--tbw-context-menu-fg",
    "--tbw-context-menu-border",
    "--tbw-context-menu-radius",
    "--tbw-context-menu-shadow",
    "--tbw-context-menu-hover",
    "--tbw-context-menu-danger",
    "--tbw-context-menu-muted",
    "--tbw-context-menu-min-width",
    "--tbw-context-menu-font-size",
    "--tbw-context-menu-font-family",
    "--tbw-context-menu-item-padding",
    "--tbw-context-menu-item-gap",
    "--tbw-context-menu-icon-size",
    "--tbw-context-menu-shortcut-size",
    "--tbw-context-menu-arrow-size"
  ];
  /**
   * Copy CSS custom properties from the grid element to the menu element.
   * This allows the context menu (appended to document.body) to inherit
   * theme variables set on tbw-grid.
   */
  copyGridStyles(e) {
    const t = this.gridElement;
    if (!t) return;
    const n = getComputedStyle(t), r = [], i = n.getPropertyValue("color-scheme").trim();
    i && r.push(`color-scheme: ${i}`);
    for (const o of S.CSS_VARS_TO_COPY) {
      const u = n.getPropertyValue(o).trim();
      u && r.push(`${o}: ${u}`);
    }
    if (r.length > 0) {
      const o = e.getAttribute("style") || "";
      e.setAttribute("style", o + r.join("; ") + ";");
    }
  }
  installGlobalHandlers() {
    !m && typeof document < "u" && typeof w == "string" && w && (m = document.createElement("style"), m.id = "tbw-context-menu-styles", m.textContent = w, document.head.appendChild(m)), h || (h = () => {
      document.querySelectorAll(".tbw-context-menu").forEach((t) => t.remove());
    }, document.addEventListener("click", h)), f || (f = (e) => {
      e.key === "Escape" && document.querySelectorAll(".tbw-context-menu").forEach((n) => n.remove());
    }, document.addEventListener("keydown", f));
  }
  /**
   * Clean up global handlers when the last instance detaches.
   * Uses reference counting to ensure handlers persist while any grid uses the plugin.
   */
  uninstallGlobalHandlers() {
    g--, !(g > 0) && (h && (document.removeEventListener("click", h), h = null), f && (document.removeEventListener("keydown", f), f = null), m && (m.remove(), m = null));
  }
  // #endregion
  // #region Hooks
  /** @internal */
  afterRender() {
    const e = this.gridElement;
    if (!e) return;
    const t = e.children[0];
    t && t.getAttribute("data-context-menu-bound") !== "true" && (t.setAttribute("data-context-menu-bound", "true"), t.addEventListener("contextmenu", (n) => {
      const r = n;
      r.preventDefault();
      const i = r.target, o = i.closest("[data-row][data-col]"), u = i.closest(".header-cell");
      let c;
      if (o) {
        const a = parseInt(o.getAttribute("data-row") ?? "-1", 10), b = parseInt(o.getAttribute("data-col") ?? "-1", 10), d = this.columns[b], E = this.rows[a];
        c = {
          row: E,
          rowIndex: a,
          column: d,
          columnIndex: b,
          field: d?.field ?? "",
          value: E?.[d?.field] ?? null,
          isHeader: !1,
          event: r
        };
      } else if (u) {
        const a = parseInt(u.getAttribute("data-col") ?? "-1", 10), b = this.columns[a];
        c = {
          row: null,
          rowIndex: -1,
          column: b,
          columnIndex: a,
          field: b?.field ?? "",
          value: null,
          isHeader: !0,
          event: r
        };
      } else
        return;
      this.params = c, this.position = { x: r.clientX, y: r.clientY };
      const l = x(this.config.items ?? p, c);
      l.length && (this.menuElement && this.menuElement.remove(), this.menuElement = v(
        l,
        c,
        (a) => {
          a.action && a.action(c), this.menuElement?.remove(), this.menuElement = null, this.isOpen = !1;
        },
        this.gridIcons.submenuArrow
      ), document.body.appendChild(this.menuElement), this.copyGridStyles(this.menuElement), C(this.menuElement, r.clientX, r.clientY), this.isOpen = !0, this.emit("context-menu-open", { params: c, items: l }));
    }));
  }
  // #endregion
  // #region Public API
  /**
   * Programmatically show the context menu at the specified position.
   * @param x - X coordinate
   * @param y - Y coordinate
   * @param params - Partial context menu parameters
   */
  showMenu(e, t, n) {
    const r = {
      row: n.row ?? null,
      rowIndex: n.rowIndex ?? -1,
      column: n.column ?? null,
      columnIndex: n.columnIndex ?? -1,
      field: n.field ?? "",
      value: n.value ?? null,
      isHeader: n.isHeader ?? !1,
      event: n.event ?? new MouseEvent("contextmenu")
    }, i = x(this.config.items ?? p, r);
    this.menuElement && this.menuElement.remove(), this.menuElement = v(
      i,
      r,
      (o) => {
        o.action && o.action(r), this.menuElement?.remove(), this.menuElement = null, this.isOpen = !1;
      },
      this.gridIcons.submenuArrow
    ), document.body.appendChild(this.menuElement), this.copyGridStyles(this.menuElement), C(this.menuElement, e, t), this.isOpen = !0;
  }
  /**
   * Hide the context menu.
   */
  hideMenu() {
    this.menuElement && (this.menuElement.remove(), this.menuElement = null, this.isOpen = !1);
  }
  /**
   * Check if the context menu is currently open.
   * @returns Whether the menu is open
   */
  isMenuOpen() {
    return this.isOpen;
  }
  // #endregion
  // Styles are injected globally via installGlobalHandlers() since menu renders in document.body
}
export {
  S as ContextMenuPlugin
};
//# sourceMappingURL=index.js.map
