view design_system/src/components/overlays.js @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents 2b6e732087ff
children
line wrap: on
line source

let nextOverlayId = 1;

function uniqueId(prefix) {
  let id;
  do id = `${prefix}-${nextOverlayId++}`;
  while (document.getElementById(id));
  return id;
}

function rememberAttribute(state, element, name) {
  let attributes = state.get(element);
  if (!attributes) {
    attributes = new Map();
    state.set(element, attributes);
  }
  if (!attributes.has(name)) {
    attributes.set(name, element.getAttribute(name));
  }
}

function setManagedAttribute(state, element, name, value) {
  rememberAttribute(state, element, name);
  if (value === null) element.removeAttribute(name);
  else element.setAttribute(name, value);
}

function restoreAttributes(state) {
  for (const [element, attributes] of state) {
    for (const [name, value] of attributes) {
      if (value === null) element.removeAttribute(name);
      else element.setAttribute(name, value);
    }
  }
  state.clear();
}

function resetManagedAttribute(state, element, name) {
  const attributes = state.get(element);
  if (!attributes?.has(name)) return;
  const value = attributes.get(name);
  if (value === null) element.removeAttribute(name);
  else element.setAttribute(name, value);
}

function ensureId(state, element, prefix) {
  const existing = element.id && document.getElementById(element.id);
  if (!element.id || (existing && existing !== element)) {
    setManagedAttribute(state, element, "id", uniqueId(prefix));
  }
  return element.id;
}

function eventElement(event) {
  return event.target instanceof Element ? event.target : null;
}

const OVERLAY_COMPONENTS = [
  "zen-dialog",
  "zen-alert-dialog",
  "zen-sheet",
  "zen-drawer",
  "zen-popover",
  "zen-hover-card",
  "zen-tooltip",
].join(",");

function ownedElement(host, selector) {
  return [...host.querySelectorAll(selector)].find(
    element => element.closest(OVERLAY_COMPONENTS) === host,
  ) || null;
}

/**
 * Wires a native modal dialog to explicit trigger and close controls.
 *
 * @extends {HTMLElement}
 */
export class ZenDialog extends HTMLElement {
  static dialogRole = null;

  constructor() {
    super();
    this._managed = new Map();
    this._open = false;
    this._openedByComponent = false;
    this._onTriggerClick = event => {
      event.preventDefault();
      this.open();
    };
    this._onDialogClick = event => {
      const control = eventElement(event)?.closest("[data-zen-close]");
      if (!control || !this._dialog?.contains(control)) return;
      const value = control.hasAttribute("value")
        ? control.getAttribute("value")
        : undefined;
      this.close(value);
    };
    this._onNativeClose = () => this.syncOpen(false);
  }

  connectedCallback() {
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true, subtree: true });
    this.sync();
  }

  sync() {
    const trigger = ownedElement(this, "button[data-zen-trigger]");
    const dialog = ownedElement(this, "dialog");
    if (trigger === this._trigger && dialog === this._dialog) return;
    if (this._openedByComponent) this.close();
    this.unbind();
    this._trigger = trigger;
    this._dialog = dialog;
    if (!this._trigger || !this._dialog) return;

    const id = ensureId(this._managed, this._dialog, this.localName);
    setManagedAttribute(
      this._managed,
      this._trigger,
      "aria-haspopup",
      "dialog",
    );
    setManagedAttribute(this._managed, this._trigger, "aria-controls", id);
    if (this.constructor.dialogRole) {
      setManagedAttribute(
        this._managed,
        this._dialog,
        "role",
        this.constructor.dialogRole,
      );
    }
    this._trigger.addEventListener("click", this._onTriggerClick);
    this._dialog.addEventListener("click", this._onDialogClick);
    this._dialog.addEventListener("close", this._onNativeClose);
    this.syncOpen(this._dialog.open);
  }

  unbind() {
    this._trigger?.removeEventListener("click", this._onTriggerClick);
    this._dialog?.removeEventListener("click", this._onDialogClick);
    this._dialog?.removeEventListener("close", this._onNativeClose);
    restoreAttributes(this._managed);
    this._trigger = null;
    this._dialog = null;
    this._open = false;
    this._openedByComponent = false;
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    if (this._openedByComponent) this.close();
    this.unbind();
  }

  open() {
    if (!this._dialog || this._dialog.open || this._open) return;
    try {
      if (typeof this._dialog.showModal === "function") {
        this._dialog.showModal();
      } else {
        setManagedAttribute(this._managed, this._dialog, "open", "");
      }
      this._openedByComponent = true;
      this.syncOpen(true);
    } catch {
      this.syncOpen(Boolean(this._dialog.open));
    }
  }

  close(returnValue) {
    if (!this._dialog || (!this._dialog.open && !this._open)) return;
    if (typeof this._dialog.close === "function") {
      if (returnValue === undefined) this._dialog.close();
      else this._dialog.close(returnValue);
    } else {
      setManagedAttribute(this._managed, this._dialog, "open", null);
    }
    this.syncOpen(false);
  }

  syncOpen(open) {
    this._open = open;
    if (!open) this._openedByComponent = false;
    if (this._trigger) {
      setManagedAttribute(
        this._managed,
        this._trigger,
        "aria-expanded",
        String(open),
      );
    }
  }
}

/**
 * Presents a native modal with alert-dialog semantics.
 *
 * @extends {ZenDialog}
 */
export class ZenAlertDialog extends ZenDialog {
  static dialogRole = "alertdialog";
}

/**
 * Presents a native dialog as an edge-aligned sheet.
 *
 * @extends {ZenDialog}
 */
export class ZenSheet extends ZenDialog {}

/**
 * Presents a native dialog as a drawer.
 *
 * @extends {ZenDialog}
 */
export class ZenDrawer extends ZenDialog {}

function isPopoverOpen(element) {
  try {
    return element.matches(":popover-open");
  } catch {
    return false;
  }
}

/**
 * Connects a trigger to a native popover with an accessible fallback.
 *
 * @extends {HTMLElement}
 */
export class ZenPopover extends HTMLElement {
  constructor() {
    super();
    this._managed = new Map();
    this._open = false;
    this._onClick = event => {
      const target = eventElement(event);
      if (!target) return;
      if (this._trigger?.contains(target)) {
        event.preventDefault();
        this.toggle();
      } else if (this._content?.contains(target) &&
                 target.closest("[data-zen-close]")) {
        this.hide();
      }
    };
    this._onToggle = event => {
      this.syncOpen(event.newState
        ? event.newState === "open"
        : isPopoverOpen(this._content));
    };
    this._onDocumentPointerDown = event => {
      if (!this._native && this._open && !this.contains(event.target)) {
        this.hide();
      }
    };
    this._onDocumentKeyDown = event => {
      if (!this._native && this._open && event.key === "Escape") {
        this.hide();
        this._trigger?.focus();
      }
    };
  }

  connectedCallback() {
    this.addEventListener("click", this._onClick);
    document.addEventListener("pointerdown", this._onDocumentPointerDown);
    document.addEventListener("keydown", this._onDocumentKeyDown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true, subtree: true });
    this.sync();
  }

  sync() {
    const trigger = ownedElement(this, "[data-zen-trigger]");
    const content = ownedElement(
      this,
      "[data-zen-content], [popover]",
    );
    if (trigger === this._trigger && content === this._content) return;
    if (this._open) this.hide();
    this.unbindElements();
    this._trigger = trigger;
    this._content = content;
    if (!this._trigger || !this._content) return;

    const id = ensureId(this._managed, this._content, "zen-popover");
    setManagedAttribute(this._managed, this._trigger, "aria-controls", id);
    this._native = typeof this._content.showPopover === "function" &&
      typeof this._content.hidePopover === "function";
    if (this._native && !this._content.hasAttribute("popover")) {
      setManagedAttribute(this._managed, this._content, "popover", "auto");
    }
    if (this._native && "popoverTargetElement" in this._trigger) {
      this._originalPopoverTarget = this._trigger.popoverTargetElement;
      this._trigger.popoverTargetElement = this._content;
    }
    if (this._native) {
      setManagedAttribute(this._managed, this._content, "hidden", null);
    } else {
      setManagedAttribute(this._managed, this._content, "hidden", "");
    }

    this._content.addEventListener("toggle", this._onToggle);
    this.syncOpen(this._native && isPopoverOpen(this._content));
  }

  unbindElements() {
    this._content?.removeEventListener("toggle", this._onToggle);
    if (this._trigger && "popoverTargetElement" in this._trigger) {
      this._trigger.popoverTargetElement = this._originalPopoverTarget || null;
    }
    restoreAttributes(this._managed);
    this._trigger = null;
    this._content = null;
    this._native = false;
    this._open = false;
    this._originalPopoverTarget = null;
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    document.removeEventListener("pointerdown", this._onDocumentPointerDown);
    document.removeEventListener("keydown", this._onDocumentKeyDown);
    if (this._open) this.hide();
    this.unbindElements();
  }

  toggle() {
    if (this._open || isPopoverOpen(this._content)) this.hide();
    else this.show();
  }

  show() {
    if (!this._content || this._open) return;
    if (this._native) {
      try {
        this._content.showPopover();
      } catch {
        this._native = false;
        setManagedAttribute(this._managed, this._content, "hidden", null);
      }
    } else {
      setManagedAttribute(this._managed, this._content, "hidden", null);
    }
    this.syncOpen(true);
  }

  hide() {
    if (!this._content || !this._open) return;
    if (this._native) {
      try {
        this._content.hidePopover();
      } catch {
        this._native = false;
        setManagedAttribute(this._managed, this._content, "hidden", "");
      }
    } else {
      setManagedAttribute(this._managed, this._content, "hidden", "");
    }
    this.syncOpen(false);
  }

  syncOpen(open) {
    this._open = open;
    if (this._trigger) {
      setManagedAttribute(
        this._managed,
        this._trigger,
        "aria-expanded",
        String(open),
      );
    }
    if (!this._native && this._content) {
      setManagedAttribute(
        this._managed,
        this._content,
        "hidden",
        open ? null : "",
      );
    }
  }
}

class ZenTimedOverlay extends HTMLElement {
  static delay = 150;
  static tooltip = false;

  constructor() {
    super();
    this._managed = new Map();
    this._timer = null;
    this._open = false;
    this._pointerWithin = false;
    this._focusWithin = false;
    this._onPointerEnter = () => {
      this._pointerWithin = true;
      this.schedule(true);
    };
    this._onPointerLeave = event => {
      if (event.relatedTarget instanceof Node &&
          this.contains(event.relatedTarget)) return;
      this._pointerWithin = false;
      this.schedule(this._focusWithin);
    };
    this._onFocusIn = () => {
      this._focusWithin = true;
      this.schedule(true);
    };
    this._onFocusOut = event => {
      if (event.relatedTarget instanceof Node &&
          this.contains(event.relatedTarget)) return;
      this._focusWithin = false;
      this.schedule(this._pointerWithin);
    };
    this._onKeyDown = event => {
      if (event.key !== "Escape" || !this._open) return;
      clearTimeout(this._timer);
      this._timer = null;
      this.setOpen(false);
    };
  }

  connectedCallback() {
    this.addEventListener("keydown", this._onKeyDown);
    this._observer ||= new MutationObserver(() => this.sync());
    this._observer.observe(this, { childList: true, subtree: true });
    this.sync();
  }

  sync() {
    const trigger = ownedElement(this, "[data-zen-trigger]");
    const content = ownedElement(this, "[data-zen-content]");
    if (trigger === this._trigger && content === this._content) return;
    this.unbindElements();
    this._trigger = trigger;
    this._content = content;
    if (!this._trigger || !this._content) return;

    const id = ensureId(this._managed, this._content, this.localName);
    setManagedAttribute(this._managed, this._content, "hidden", "");
    if (this.constructor.tooltip) {
      setManagedAttribute(this._managed, this._content, "role", "tooltip");
      const ids = new Set(
        (this._trigger.getAttribute("aria-describedby") || "")
          .split(/\s+/)
          .filter(Boolean),
      );
      ids.add(id);
      setManagedAttribute(
        this._managed,
        this._trigger,
        "aria-describedby",
        [...ids].join(" "),
      );
    } else {
      setManagedAttribute(this._managed, this._trigger, "aria-controls", id);
      setManagedAttribute(
        this._managed,
        this._trigger,
        "aria-expanded",
        "false",
      );
    }

    for (const element of [this._trigger, this._content]) {
      element.addEventListener("pointerenter", this._onPointerEnter);
      element.addEventListener("pointerleave", this._onPointerLeave);
      element.addEventListener("focusin", this._onFocusIn);
      element.addEventListener("focusout", this._onFocusOut);
    }
  }

  unbindElements() {
    clearTimeout(this._timer);
    this._timer = null;
    for (const element of [this._trigger, this._content]) {
      element?.removeEventListener("pointerenter", this._onPointerEnter);
      element?.removeEventListener("pointerleave", this._onPointerLeave);
      element?.removeEventListener("focusin", this._onFocusIn);
      element?.removeEventListener("focusout", this._onFocusOut);
    }
    restoreAttributes(this._managed);
    this._trigger = null;
    this._content = null;
    this._open = false;
    this._pointerWithin = false;
    this._focusWithin = false;
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("keydown", this._onKeyDown);
    this.unbindElements();
  }

  schedule(open) {
    clearTimeout(this._timer);
    this._timer = setTimeout(
      () => this.setOpen(open),
      this.constructor.delay,
    );
  }

  setOpen(open) {
    if (!this._content || open === this._open) return;
    this._open = open;
    setManagedAttribute(
      this._managed,
      this._content,
      "hidden",
      open ? null : "",
    );
    if (!this.constructor.tooltip) {
      setManagedAttribute(
        this._managed,
        this._trigger,
        "aria-expanded",
        String(open),
      );
    }
  }
}

/**
 * Shows supplemental interactive content on hover or focus.
 *
 * @extends {ZenTimedOverlay}
 */
export class ZenHoverCard extends ZenTimedOverlay {
  static delay = 150;
}

/**
 * Shows a delayed non-interactive description on hover or focus.
 *
 * @extends {ZenTimedOverlay}
 */
export class ZenTooltip extends ZenTimedOverlay {
  static delay = 350;
  static tooltip = true;
}

const MENU_COMPONENTS = [
  "zen-dropdown-menu",
  "zen-context-menu",
  "zen-menubar",
  "zen-navigation-menu",
].join(",");
const MENU_SELECTOR = "[data-zen-menu], [role='menu'], [data-zen-content]";
const ITEM_SELECTOR = [
  "[role='menuitem']",
  "[role='menuitemcheckbox']",
  "[role='menuitemradio']",
  "[data-zen-menu-item]",
  "a[href]",
  "button",
  "input[type='button']",
  "input[type='submit']",
].join(",");

class ZenMenu extends HTMLElement {
  static contextMenu = false;
  static menubar = false;
  static multiple = false;

  constructor() {
    super();
    this._managed = new Map();
    this._pairs = [];
    this._openPair = null;
    this._onClick = event => {
      const target = eventElement(event);
      if (!target) return;
      const pair = this._pairs.find(({ trigger }) =>
        trigger.contains(target));
      if (pair) {
        event.preventDefault();
        if (this._openPair === pair) this.close(false);
        else this.open(pair, 0, this.constructor.contextMenu ? event : null);
        return;
      }
      const item = target.closest(ITEM_SELECTOR);
      if (this._openPair?.menu.contains(target) &&
          item &&
          !item.hasAttribute("disabled") &&
          item.getAttribute("aria-disabled") !== "true") {
        this.close(true);
      }
    };
    this._onContextMenu = event => {
      if (!this.constructor.contextMenu ||
          this._pairs.some(({ menu }) => menu.contains(event.target))) return;
      event.preventDefault();
      this.open(this._pairs[0], 0, event);
    };
    this._onKeyDown = event => this.handleKeyDown(event);
    this._onDocumentPointerDown = event => {
      if (!this._openPair) return;
      const { trigger, menu } = this._openPair;
      if (!trigger.contains(event.target) && !menu.contains(event.target)) {
        this.close(false);
      }
    };
  }

  connectedCallback() {
    this.setupPairs();
    this.addEventListener("click", this._onClick);
    this.addEventListener("contextmenu", this._onContextMenu);
    this.addEventListener("keydown", this._onKeyDown);
    document.addEventListener("pointerdown", this._onDocumentPointerDown);
    this._observer ||= new MutationObserver(() => this.refreshPairs());
    this._observer.observe(this, { childList: true, subtree: true });
  }

  disconnectedCallback() {
    this._observer?.disconnect();
    this.removeEventListener("click", this._onClick);
    this.removeEventListener("contextmenu", this._onContextMenu);
    this.removeEventListener("keydown", this._onKeyDown);
    document.removeEventListener("pointerdown", this._onDocumentPointerDown);
    this._openPair = null;
    restoreAttributes(this._managed);
    this._pairs = [];
  }

  refreshPairs() {
    if (this._openPair) this.close(false);
    restoreAttributes(this._managed);
    this._pairs = [];
    this.setupPairs();
  }

  ownedElements(selector) {
    return [...this.querySelectorAll(selector)].filter(element =>
      element.closest(MENU_COMPONENTS) === this);
  }

  setupPairs() {
    let triggers = this.ownedElements("[data-zen-trigger]");
    const menus = this.ownedElements(MENU_SELECTOR);
    triggers = triggers.filter(trigger =>
      !menus.some(menu => menu.contains(trigger)));
    if (!this.constructor.multiple) triggers = triggers.slice(0, 1);

    const unused = new Set(menus);
    for (const trigger of triggers) {
      const controlled = trigger.getAttribute("aria-controls");
      let menu = controlled
        ? menus.find(candidate => candidate.id === controlled)
        : null;
      if (!menu) menu = [...unused][0];
      if (!menu) continue;
      unused.delete(menu);

      const triggerId = ensureId(this._managed, trigger, `${this.localName}-trigger`);
      const menuId = ensureId(this._managed, menu, `${this.localName}-menu`);
      setManagedAttribute(this._managed, trigger, "aria-haspopup", "menu");
      setManagedAttribute(this._managed, trigger, "aria-controls", menuId);
      setManagedAttribute(this._managed, trigger, "aria-expanded", "false");
      setManagedAttribute(this._managed, menu, "role", "menu");
      setManagedAttribute(this._managed, menu, "aria-labelledby", triggerId);
      setManagedAttribute(this._managed, menu, "hidden", "");
      if (this.constructor.menubar) {
        setManagedAttribute(this._managed, trigger, "role", "menuitem");
      }
      const pair = { trigger, menu };
      this._pairs.push(pair);
      this.prepareItems(pair);
    }
    if (this.constructor.menubar) {
      setManagedAttribute(this._managed, this, "role", "menubar");
      this.setMenubarTabStop(this._pairs[0]);
    }
  }

  prepareItems(pair) {
    const items = [...pair.menu.querySelectorAll(ITEM_SELECTOR)].filter(item =>
      item.closest(MENU_SELECTOR) === pair.menu &&
      !item.hidden &&
      !item.hasAttribute("disabled") &&
      item.getAttribute("aria-disabled") !== "true");
    for (const item of items) {
      if (!item.hasAttribute("role")) {
        setManagedAttribute(this._managed, item, "role", "menuitem");
      }
      setManagedAttribute(this._managed, item, "tabindex", "-1");
    }
    return items;
  }

  open(pair, itemIndex = 0, pointerEvent = null) {
    if (!pair) return;
    if (this._openPair && this._openPair !== pair) this.close(false);
    this._openPair = pair;
    if (this.constructor.menubar) this.setMenubarTabStop(pair);
    if (pointerEvent) {
      rememberAttribute(this._managed, pair.menu, "style");
      pair.menu.style.position = "fixed";
      pair.menu.style.left = `${Number(pointerEvent.clientX) || 0}px`;
      pair.menu.style.top = `${Number(pointerEvent.clientY) || 0}px`;
    } else {
      resetManagedAttribute(this._managed, pair.menu, "style");
    }
    setManagedAttribute(this._managed, pair.menu, "hidden", null);
    setManagedAttribute(this._managed, pair.trigger, "aria-expanded", "true");
    const items = this.prepareItems(pair);
    if (items.length) {
      const index = itemIndex < 0 ? items.length - 1 : itemIndex;
      setManagedAttribute(this._managed, items[index], "tabindex", "0");
      items[index].focus();
    } else {
      setManagedAttribute(this._managed, pair.menu, "tabindex", "-1");
      pair.menu.focus();
    }
  }

  close(restoreFocus) {
    if (!this._openPair) return;
    const pair = this._openPair;
    this._openPair = null;
    setManagedAttribute(this._managed, pair.menu, "hidden", "");
    setManagedAttribute(this._managed, pair.trigger, "aria-expanded", "false");
    resetManagedAttribute(this._managed, pair.menu, "style");
    if (restoreFocus) pair.trigger.focus();
  }

  moveItem(pair, offset, edge = null) {
    const active = document.activeElement;
    const items = this.prepareItems(pair);
    if (!items.length) return;
    let index = items.indexOf(active);
    if (edge === "first") index = 0;
    else if (edge === "last") index = items.length - 1;
    else index = (Math.max(index, 0) + offset + items.length) % items.length;
    setManagedAttribute(this._managed, items[index], "tabindex", "0");
    items[index].focus();
  }

  moveMenubarTrigger(pair, offset, openMenu) {
    const index = this._pairs.indexOf(pair);
    const next = this._pairs[
      (index + offset + this._pairs.length) % this._pairs.length
    ];
    this.setMenubarTabStop(next);
    if (openMenu) this.open(next);
    else next.trigger.focus();
  }

  setMenubarTabStop(activePair) {
    for (const pair of this._pairs) {
      setManagedAttribute(
        this._managed,
        pair.trigger,
        "tabindex",
        pair === activePair ? "0" : "-1",
      );
    }
  }

  handleKeyDown(event) {
    const target = eventElement(event);
    if (!target) return;
    const triggerPair = this._pairs.find(({ trigger }) =>
      trigger.contains(target));
    if (triggerPair) {
      if (this.constructor.menubar &&
          (event.key === "ArrowRight" || event.key === "ArrowLeft")) {
        event.preventDefault();
        this.moveMenubarTrigger(
          triggerPair,
          event.key === "ArrowRight" ? 1 : -1,
          false,
        );
      } else if (["Enter", " ", "ArrowDown", "ArrowUp"].includes(event.key)) {
        event.preventDefault();
        this.open(triggerPair, event.key === "ArrowUp" ? -1 : 0);
      } else if (event.key === "Escape" && this._openPair) {
        event.preventDefault();
        this.close(true);
      }
      return;
    }

    const pair = this._pairs.find(({ menu }) => menu.contains(target));
    if (!pair || this._openPair !== pair) return;
    if (event.key === "ArrowDown") {
      event.preventDefault();
      this.moveItem(pair, 1);
    } else if (event.key === "ArrowUp") {
      event.preventDefault();
      this.moveItem(pair, -1);
    } else if (event.key === "Home" || event.key === "End") {
      event.preventDefault();
      this.moveItem(pair, 0, event.key === "Home" ? "first" : "last");
    } else if (event.key === "Escape") {
      event.preventDefault();
      this.close(true);
    } else if (event.key === "Tab") {
      this.close(false);
    } else if (this.constructor.menubar &&
               (event.key === "ArrowRight" || event.key === "ArrowLeft")) {
      event.preventDefault();
      this.moveMenubarTrigger(
        pair,
        event.key === "ArrowRight" ? 1 : -1,
        true,
      );
    }
  }
}

/**
 * Provides an accessible action menu opened from a native trigger.
 *
 * @extends {ZenMenu}
 */
export class ZenDropdownMenu extends ZenMenu {}

/**
 * Opens an accessible action menu at the pointer location.
 *
 * @extends {ZenMenu}
 */
export class ZenContextMenu extends ZenMenu {
  static contextMenu = true;
}

/**
 * Coordinates multiple keyboard-accessible application menus.
 *
 * @extends {ZenMenu}
 */
export class ZenMenubar extends ZenMenu {
  static menubar = true;
  static multiple = true;
}

/**
 * Provides keyboard-accessible navigation flyouts.
 *
 * @extends {ZenMenu}
 */
export class ZenNavigationMenu extends ZenMenu {}

const definitions = {
  "zen-dialog": ZenDialog,
  "zen-alert-dialog": ZenAlertDialog,
  "zen-sheet": ZenSheet,
  "zen-drawer": ZenDrawer,
  "zen-popover": ZenPopover,
  "zen-hover-card": ZenHoverCard,
  "zen-tooltip": ZenTooltip,
  "zen-dropdown-menu": ZenDropdownMenu,
  "zen-context-menu": ZenContextMenu,
  "zen-menubar": ZenMenubar,
  "zen-navigation-menu": ZenNavigationMenu,
};

for (const [name, constructor] of Object.entries(definitions)) {
  if (!customElements.get(name)) customElements.define(name, constructor);
}