const g = {
  expand: "▶",
  collapse: "▼",
  sortAsc: "▲",
  sortDesc: "▼",
  sortNone: "⇅",
  submenuArrow: "▶",
  dragHandle: "⋮⋮",
  toolPanel: "☰"
};
class p {
  /**
   * 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 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, r) {
    this.grid?.dispatchEvent?.(new CustomEvent(e, { detail: r, bubbles: !0 }));
  }
  /**
   * Emit a cancelable custom event from the grid.
   * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise
   */
  emitCancelable(e, r) {
    const t = new CustomEvent(e, { detail: r, bubbles: !0, cancelable: !0 });
    return this.grid?.dispatchEvent?.(t), t.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 render root of the grid for DOM queries.
   * @deprecated Use `gridElement` instead. This getter exists only for backward compatibility.
   *
   * With Shadow DOM removed, the grid element itself is the render root.
   * All new code should use `this.gridElement` for DOM queries.
   *
   * @example
   * // OLD (deprecated)
   * const rows = this.shadowRoot?.querySelector('.rows');
   *
   * // NEW (preferred)
   * const rows = this.gridElement.querySelector('.rows');
   */
  get shadowRoot() {
    return this.gridElement;
  }
  /**
   * 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 { ...g, ...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 r = this.gridElement;
    return r ? getComputedStyle(r).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 r = getComputedStyle(e).getPropertyValue("--tbw-animation-duration").trim(), t = parseInt(r, 10);
      if (!isNaN(t)) return t;
    }
    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, r) {
    return r !== void 0 ? r : 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, r) {
    typeof r == "string" ? e.innerHTML = r : r instanceof HTMLElement && (e.innerHTML = "", e.appendChild(r.cloneNode(!0)));
  }
  /**
   * Log a warning message.
   */
  warn(e) {
    console.warn(`[tbw-grid:${this.name}] ${e}`);
  }
  // #endregion
}
function c(u) {
  if (!u.length) return [];
  const e = /* @__PURE__ */ new Map(), r = [], t = (o, l) => {
    if (!l.length) return;
    const s = r[r.length - 1];
    if (s && s.implicit && s.firstIndex + s.columns.length === o) {
      s.columns.push(...l);
      return;
    }
    r.push({
      id: "__implicit__" + o,
      label: void 0,
      columns: l,
      firstIndex: o,
      implicit: !0
    });
  };
  let i = [], n = 0;
  return u.forEach((o, l) => {
    const s = o.group;
    if (!s) {
      i.length === 0 && (n = l), i.push(o);
      return;
    }
    i.length && (t(n, i.slice()), i = []);
    const d = typeof s == "string" ? s : s.id;
    let a = e.get(d);
    a || (a = {
      id: d,
      label: typeof s == "string" ? void 0 : s.label,
      columns: [],
      firstIndex: l
    }, e.set(d, a), r.push(a)), a.columns.push(o);
  }), i.length && t(n, i), r.length === 1 && r[0].implicit && r[0].columns.length === u.length ? [] : r;
}
function h(u, e, r) {
  if (!e.length || !u) return;
  const t = /* @__PURE__ */ new Map();
  for (const n of e)
    for (const o of n.columns)
      o.field && t.set(o.field, n.id);
  const i = Array.from(u.querySelectorAll(".cell[data-field]"));
  i.forEach((n) => {
    const o = n.getAttribute("data-field") || "", l = t.get(o);
    l && (n.classList.add("grouped"), n.getAttribute("data-group") || n.setAttribute("data-group", l));
  });
  for (const n of e) {
    const o = n.columns[n.columns.length - 1], l = i.find((s) => s.getAttribute("data-field") === o.field);
    l && l.classList.add("group-end");
  }
}
function f(u, e) {
  if (u.length === 0) return null;
  const r = document.createElement("div");
  r.className = "header-group-row", r.setAttribute("role", "row");
  for (const t of u) {
    const i = t.columns[0], n = i ? e.findIndex((d) => d.field === i.field) : -1;
    if (n === -1) continue;
    const o = String(t.id).startsWith("__implicit__"), l = o ? "" : t.label || t.id, s = document.createElement("div");
    s.className = "cell header-group-cell", o && s.classList.add("implicit-group"), s.setAttribute("data-group", String(t.id)), s.style.gridColumn = `${n + 1} / span ${t.columns.length}`, s.textContent = l, r.appendChild(s);
  }
  return r;
}
function m(u) {
  return u.some((e) => e.group != null);
}
const b = "@layer tbw-plugins{.header-group-row{display:grid;grid-auto-flow:column;background:var(--tbw-grouping-columns-header-bg, var(--tbw-color-header-bg));border-bottom:1px solid var(--tbw-grouping-columns-border, var(--tbw-color-border))}.header-group-cell{display:flex;align-items:center;justify-content:center;padding:var(--tbw-button-padding-sm, .25rem .5rem);font-weight:600;font-size:var(--tbw-font-size-sm, .9em);text-transform:uppercase;letter-spacing:.5px;border-right:2px solid var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)))}.header-group-cell:last-child{border-right:none}.header-row .cell.grouped{border-top:none}.header-row .cell.group-end{border-right:2px solid var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)))}.header-row .cell.group-end:last-child{border-right:none}.rows .cell.group-end{border-right:2px solid var(--tbw-grouping-columns-separator, var(--tbw-color-border-strong, var(--tbw-color-border)))}.rows .cell.group-end:last-child{border-right:none}.header-group-row.no-borders{border-bottom:none}.header-group-row.no-borders .header-group-cell{border-right:none}.header-row.no-group-borders .cell.group-end{border-right:1px solid var(--tbw-color-border)}}";
class w extends p {
  name = "groupingColumns";
  styles = b;
  get defaultConfig() {
    return {
      showGroupBorders: !0
    };
  }
  // #region Internal State
  groups = [];
  isActive = !1;
  // #endregion
  // #region Lifecycle
  detach() {
    this.groups = [], this.isActive = !1;
  }
  // #endregion
  // #region Static Detection
  /**
   * Auto-detect column groups from column configuration.
   * Detects both inline `column.group` properties and declarative `columnGroups` config.
   */
  static detect(e, r) {
    if (r?.columnGroups && Array.isArray(r.columnGroups) && r.columnGroups.length > 0)
      return !0;
    const t = r?.columns;
    return Array.isArray(t) ? m(t) : !1;
  }
  // #endregion
  // #region Hooks
  processColumns(e) {
    const r = this.grid?.gridConfig?.columnGroups;
    let t;
    if (r && Array.isArray(r) && r.length > 0) {
      const n = /* @__PURE__ */ new Map();
      for (const o of r)
        for (const l of o.children)
          n.set(l, { id: o.id, label: o.header });
      t = e.map((o) => {
        const l = n.get(o.field);
        return l && !o.group ? { ...o, group: l } : o;
      });
    } else
      t = [...e];
    const i = c(t);
    return i.length === 0 ? (this.isActive = !1, this.groups = [], t) : (this.isActive = !0, this.groups = i, t);
  }
  afterRender() {
    if (!this.isActive) {
      const s = this.gridElement?.querySelector(".header")?.querySelector(".header-group-row");
      s && s.remove();
      return;
    }
    const e = this.gridElement?.querySelector(".header");
    if (!e) return;
    const r = e.querySelector(".header-group-row");
    r && r.remove();
    const t = this.columns, i = c(t);
    if (i.length === 0) return;
    const n = f(i, t);
    if (n) {
      n.classList.toggle("no-borders", !this.config.showGroupBorders);
      const l = e.querySelector(".header-row");
      l ? e.insertBefore(n, l) : e.appendChild(n);
    }
    const o = e.querySelector(".header-row");
    o && (o.classList.toggle("no-group-borders", !this.config.showGroupBorders), h(o, i)), this.#e(i);
  }
  /**
   * Apply group-end class to all data cells in the last column of each group.
   * This extends the strong border separator through all data rows.
   */
  #e(e) {
    if (!this.config.showGroupBorders) return;
    const r = this.gridElement;
    if (!r) return;
    const t = /* @__PURE__ */ new Set();
    for (const n of e) {
      const o = n.columns[n.columns.length - 1];
      o?.field && t.add(o.field);
    }
    const i = r.querySelectorAll(".rows .cell[data-field]");
    for (const n of i) {
      const o = n.getAttribute("data-field");
      o && t.has(o) ? n.classList.add("group-end") : n.classList.remove("group-end");
    }
  }
  // #endregion
  // #region Public API
  /**
   * Check if column groups are active.
   * @returns Whether grouping is active
   */
  isGroupingActive() {
    return this.isActive;
  }
  /**
   * Get the computed column groups.
   * @returns Array of column groups
   */
  getGroups() {
    return this.groups;
  }
  /**
   * Get columns in a specific group.
   * @param groupId - The group ID to find
   * @returns Array of columns in the group
   */
  getGroupColumns(e) {
    const r = this.groups.find((t) => t.id === e);
    return r ? r.columns : [];
  }
  /**
   * Refresh column groups (recompute from current columns).
   */
  refresh() {
    this.requestRender();
  }
  // #endregion
}
export {
  w as GroupingColumnsPlugin
};
//# sourceMappingURL=index.js.map
