UNPKG

zircle

Version:

Zircle's circular components, rebuilt with Orbit and Zumly for the web. No Vue required.

1,738 lines 69.3 kB
/*! Zircle, Orbit, and Zumly are MIT licensed. See LICENSES.txt for copyright and permission notices. */

// src/components/index.js
import { registerOrbit } from "@zumer/orbit";

// src/core.js
import { Zumly, ZumlyRouter } from "zumly";
import { Orbit } from "@zumer/orbit";

// src/components/sizes.js
var SIZE_RATIOS = Object.freeze({
  xxl: 1,
  xl: 260 / 420,
  l: 160 / 420,
  m: 100 / 420,
  s: 62 / 420,
  xs: 38 / 420,
  xxs: 16 / 420
});
var SIZES = Object.freeze(Object.keys(SIZE_RATIOS));
var aliases = Object.freeze({
  extralarge: "xl",
  large: "l",
  medium: "m",
  small: "s",
  extrasmall: "xs"
});
function normaliseSize(value, fallback = "m") {
  const key = String(value ?? "").trim().toLowerCase();
  const size = aliases[key] || key;
  return SIZES.includes(size) ? size : SIZES.includes(fallback) ? fallback : "m";
}
function sizeVariable(value, fallback = "m") {
  return `var(--z-size-${normaliseSize(value, fallback)})`;
}

// src/components/surface.js
var HTMLElementBase = globalThis.HTMLElement || class {
};
var own = (node, key) => Object.prototype.hasOwnProperty.call(node, key);
var booleanAttributes = /* @__PURE__ */ new Set(["circle", "square", "button", "disabled", "slider", "knob"]);
var attributes = [
  "size",
  "distance",
  "angle",
  "circle",
  "square",
  "label",
  "label-pos",
  "image-path",
  "to-view",
  "button",
  "disabled",
  "slider",
  "progress",
  "knob",
  "qty",
  "unit",
  "min",
  "max",
  "step",
  "pos"
];
var defaults = { distance: 100, angle: 0, progress: 0, qty: 0, min: 0, max: 100, step: 1 };
var properties = Object.fromEntries(attributes.map((attribute) => [
  attribute.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()),
  attribute
]));
function numberAttribute(element, name, fallback) {
  const text = element.getAttribute(name);
  const value = text === null || text.trim() === "" ? fallback : Number(text);
  return Number.isFinite(value) ? value : fallback;
}
function part(parent, name, tag = "div", className = "") {
  let node = Array.from(parent.children).find((child) => child.dataset.zPart === name);
  if (!node) {
    node = parent.ownerDocument.createElement(tag);
    node.dataset.zPart = name;
    if (className) node.className = className;
    parent.append(node);
  }
  return node;
}
function isDefinition(element) {
  const view = element.localName === "z-view" ? element : element.closest("z-view");
  return view?.hasAttribute("name") && !view.hasAttribute("data-view-name") && view.parentElement?.localName === "z-canvas";
}
function logicalParent(element) {
  return element.parentElement?.closest("z-view, z-spot, z-list") || null;
}
function setAttribute(element, name, value) {
  if (element.getAttribute(name) !== String(value)) element.setAttribute(name, String(value));
}
var ZSurface = class _ZSurface extends HTMLElementBase {
  static get observedAttributes() {
    return attributes;
  }
  constructor() {
    super();
    this._surfaceUpdating = false;
    this._surfaceQueued = false;
    this._surfaceListening = false;
    this._surfaceParts = null;
    this._surfaceObserver = null;
    this._surfaceResizeObserver = null;
    this._onSurfaceAction = (event) => this._handleAction(event);
    this._onSurfaceDisabled = (event) => {
      if (this.hasAttribute("disabled") && event.target.closest?.("z-spot") === this) {
        event.preventDefault();
        event.stopImmediatePropagation();
      }
    };
    this._onSurfaceKey = (event) => this._handleKey(event);
    this._onSurfaceValue = (event) => this._handleValue(event);
    this._onSurfaceScroll = () => this._syncScrollValue();
  }
  connectedCallback() {
    if (isDefinition(this)) return;
    this.refresh();
  }
  disconnectedCallback() {
    queueMicrotask(() => {
      if (this.isConnected) return;
      this._surfaceObserver?.disconnect();
      this._surfaceResizeObserver?.disconnect();
      this._unlisten();
    });
  }
  attributeChangedCallback() {
    if (this.isConnected && !isDefinition(this)) this.refresh();
  }
  get contentElement() {
    return this._surfaceParts?.content || null;
  }
  refresh() {
    if (this._surfaceUpdating || !this.isConnected || isDefinition(this)) return this;
    this._surfaceUpdating = true;
    this._surfaceObserver?.disconnect();
    try {
      for (const property of Object.keys(properties)) {
        if (own(this, property)) {
          const value = this[property];
          delete this[property];
          this[property] = value;
        }
      }
      this.classList.add("z-surface", `z-${this.surfaceType}`);
      if (this.surfaceType === "spot") this.classList.add("satellite");
      const body = part(this, "surface", "div", "z-surface-body");
      this._surfaceParts = {
        body,
        plate: part(this, "plate", "div", "z-plate"),
        image: part(body, "image", "div", "z-image"),
        content: part(body, "content", "div", "z-content"),
        media: part(body, "media", "div", "z-media"),
        extensions: part(this, "extensions", "div", "z-extensions")
      };
      this._adoptContent();
      this._syncAppearance();
      this._syncControls();
      this._syncPosition();
      this._syncScroll();
      this._listen();
    } finally {
      this._surfaceUpdating = false;
      this._observe();
    }
    return this;
  }
  _adoptContent() {
    const slots = this._surfaceParts;
    const authored = [this, slots.content, slots.image, slots.media, slots.extensions].flatMap((holder) => Array.from(holder.childNodes));
    for (const node of authored) {
      if (node.nodeType === 1 && node.hasAttribute("data-z-part")) continue;
      const slot = node.nodeType === 1 ? node.getAttribute("slot") : null;
      const radial = node.nodeType === 1 && node.matches("z-spot, z-list");
      const target = slot === "image" ? slots.image : slot === "media" ? slots.media : slot === "extension" || radial ? slots.extensions : slots.content;
      if (node.parentNode !== target) target.append(node);
    }
    for (const node of Array.from(slots.extensions.children)) {
      if (node.localName === "z-spot") this._adoptSpot(node);
    }
  }
  _adoptSpot(spot) {
    const extensions = this._surfaceParts?.extensions;
    if (!extensions || spot === this) return;
    const layout = part(extensions, "layout", "div", "bigbang z-orbit-layout");
    const gravity = part(layout, "gravity", "div", "gravity-spot");
    let ring = spot.parentElement;
    if (ring?.dataset.zPart !== "spot-orbit" || ring.parentElement !== gravity) {
      ring = this.ownerDocument.createElement("div");
      ring.className = "orbit-12 z-spot-orbit";
      ring.dataset.zPart = "spot-orbit";
      gravity.append(ring);
      ring.append(spot);
    }
    this._sizeSpotOrbit(spot, ring);
  }
  _sizeSpotOrbit(spot, ring) {
    const size = sizeVariable(this.getAttribute("size"), this.defaultSize);
    const distance = numberAttribute(spot, "distance", 100);
    ring.style.setProperty("--o-force", `calc(${size} * ${distance / 100})`);
    ring.style.setProperty("--o-force-ratio", "1");
    ring.style.setProperty("--o-range", "0deg");
  }
  _syncPosition() {
    const size = normaliseSize(this.getAttribute("size"), this.defaultSize);
    this.style.setProperty("--z-diameter", sizeVariable(size));
    this.dataset.zSize = size;
    if (this.surfaceType === "spot") {
      this.style.setProperty("--o-from", `${numberAttribute(this, "angle", 0)}deg`);
      this.style.setProperty("--o-offset", "0deg");
      const parent = logicalParent(this);
      if (parent instanceof _ZSurface) parent._adoptSpot(this);
    }
    const gravity = this._surfaceParts.extensions.querySelector(':scope > [data-z-part="layout"] > [data-z-part="gravity"]');
    if (gravity) {
      for (const ring of gravity.children) {
        if (ring.dataset.zPart !== "spot-orbit") continue;
        const spot = Array.from(ring.children).find((child) => child.localName === "z-spot");
        if (spot) this._sizeSpotOrbit(spot, ring);
        else ring.remove();
      }
    }
  }
  _syncAppearance() {
    const square = this.hasAttribute("square");
    this.classList.toggle("is-square", square);
    this.classList.toggle("is-circle", !square);
    this.classList.toggle("z-button", this.hasAttribute("button"));
    const disabled = this.hasAttribute("disabled");
    this.classList.toggle("is-disabled", disabled);
    const target = this.getAttribute("to-view");
    const navigable = this.surfaceType === "spot" && Boolean(target) && !disabled;
    this.classList.toggle("zoom-me", navigable);
    if (navigable) this.dataset.to = target;
    else delete this.dataset.to;
    if (this.surfaceType === "spot") {
      const button = Boolean(target) || this.hasAttribute("button");
      if (button) {
        setAttribute(this, "role", "button");
        setAttribute(this, "tabindex", disabled ? "-1" : "0");
        setAttribute(this, "aria-disabled", disabled ? "true" : "false");
        this.dataset.zInteractive = "";
      } else if (this.hasAttribute("data-z-interactive")) {
        this.removeAttribute("role");
        this.removeAttribute("tabindex");
        this.removeAttribute("aria-disabled");
        delete this.dataset.zInteractive;
      }
    }
    const image = this._surfaceParts.image;
    let source = Array.from(image.children).find((node) => node.dataset.zPart === "image-source");
    const path = this.getAttribute("image-path");
    if (path) {
      source ||= part(image, "image-source", "img", "z-image-source");
      setAttribute(source, "src", path);
      setAttribute(source, "alt", "");
    } else source?.remove();
    image.hidden = !image.childNodes.length;
    this._surfaceParts.media.hidden = !this._surfaceParts.media.childNodes.length;
    const labelText = this.getAttribute("label") || "";
    const quantityOutside = this.getAttribute("pos") === "outside" && this.hasAttribute("qty") && !this.hasAttribute("knob");
    let label = Array.from(this.children).find((node) => node.dataset.zPart === "label");
    if (labelText || quantityOutside) {
      label ||= part(this, "label", "div", "z-label");
      const position = this.getAttribute("label-pos") || "bottom";
      for (const token of ["top", "bottom", "left", "right", "center"]) label.classList.toggle(token, token === position);
      label.dataset.position = position;
      const inside = part(label, "label-text", "span", "inside");
      if (inside.textContent !== labelText) inside.textContent = labelText;
    } else label?.remove();
    let quantity = this.querySelector(':scope > [data-z-part="label"] > [data-z-part="quantity"], :scope > [data-z-part="surface"] > [data-z-part="content"] > [data-z-part="quantity"]');
    if (this.hasAttribute("qty") && !this.hasAttribute("knob")) {
      const holder = quantityOutside ? label : this._surfaceParts.content;
      if (!quantity) quantity = part(holder, "quantity", "span", "z-quantity");
      else if (quantity.parentElement !== holder) holder.append(quantity);
      const text = `${numberAttribute(this, "qty", 0)}${this.getAttribute("unit") || ""}`;
      if (quantity.textContent !== text) quantity.textContent = text;
    } else quantity?.remove();
  }
  _syncControls() {
    for (const [flag, tag, className, forwarded] of [
      ["slider", "z-slider", "z-surface-slider", ["progress"]],
      ["knob", "z-knob", "z-surface-knob", ["qty", "unit", "min", "max", "step", "disabled"]]
    ]) {
      let control = Array.from(this.children).find((node) => node.dataset.zPart === flag);
      if (this.hasAttribute(flag) && (flag !== "slider" || !this.hasAttribute("square"))) {
        control ||= part(this, flag, tag, className);
        for (const attribute of forwarded) {
          if (this.hasAttribute(attribute)) setAttribute(control, attribute, this.getAttribute(attribute));
          else control.removeAttribute(attribute);
        }
        setAttribute(control, "aria-label", this.getAttribute("aria-label") || this.getAttribute("label") || (flag === "knob" ? "Value" : "Progress"));
      } else control?.remove();
    }
    this._surfaceParts.content.hidden = this.hasAttribute("knob");
  }
  _syncScroll() {
    if (this.surfaceType !== "view") return;
    const content = this._surfaceParts.content;
    const square = this.hasAttribute("square") || !this.hasAttribute("circle") && this.closest("[data-shape]")?.dataset.shape === "square";
    const overflowing = !square && content.clientHeight > 0 && content.scrollHeight > content.clientHeight + 1;
    let control = Array.from(this.children).find((node) => node.dataset.zPart === "scroll");
    if (overflowing) {
      control ||= part(this, "scroll", "z-scroll", "z-surface-scroll");
      setAttribute(control, "aria-label", "Scroll content");
      this._syncScrollValue();
    } else control?.remove();
  }
  _syncScrollValue() {
    const control = Array.from(this.children).find((node) => node.dataset.zPart === "scroll");
    if (!control || !this._surfaceParts) return;
    const content = this._surfaceParts.content;
    const range = content.scrollHeight - content.clientHeight;
    setAttribute(control, "scroll-val", range > 0 ? -45 + 90 * content.scrollTop / range : -45);
  }
  _listen() {
    if (this._surfaceListening) return;
    this._surfaceListening = true;
    for (const type of ["mouseup", "touchend", "click"]) {
      this.addEventListener(type, this._onSurfaceAction);
      this.addEventListener(type, this._onSurfaceDisabled, true);
    }
    this.addEventListener("keydown", this._onSurfaceKey);
    this.addEventListener("input", this._onSurfaceValue, true);
    this.addEventListener("change", this._onSurfaceValue, true);
    this._surfaceParts.content.addEventListener("scroll", this._onSurfaceScroll, { passive: true });
  }
  _unlisten() {
    if (!this._surfaceListening) return;
    this._surfaceListening = false;
    for (const type of ["mouseup", "touchend", "click"]) {
      this.removeEventListener(type, this._onSurfaceAction);
      this.removeEventListener(type, this._onSurfaceDisabled, true);
    }
    this.removeEventListener("keydown", this._onSurfaceKey);
    this.removeEventListener("input", this._onSurfaceValue, true);
    this.removeEventListener("change", this._onSurfaceValue, true);
    this._surfaceParts?.content.removeEventListener("scroll", this._onSurfaceScroll);
  }
  _handleAction(event) {
    if (this.surfaceType !== "spot") return;
    if (event.target.closest?.("z-spot") !== this) return;
    const control = event.target.closest?.("button, a, input, select, textarea, z-knob, z-scroll");
    if (event.type === "click" && !this.getAttribute("to-view") && !this.parentElement?.closest(".zoom-me")) return;
    if (this.hasAttribute("disabled") || !this.getAttribute("to-view") || control && control !== this) {
      event.stopPropagation();
      if (this.hasAttribute("disabled")) event.preventDefault();
    }
  }
  _handleKey(event) {
    if (this.surfaceType !== "spot" || event.target !== this) return;
    if (this.hasAttribute("disabled")) {
      if (event.key === "Enter" || event.key === " ") {
        event.preventDefault();
        event.stopPropagation();
      }
      return;
    }
    if (this.getAttribute("to-view") || !this.hasAttribute("button")) return;
    if ((event.key === "Enter" || event.key === " ") && !event.repeat) {
      event.preventDefault();
      event.stopPropagation();
      this.click();
    }
  }
  _handleValue(event) {
    if (event.target.parentElement === this && event.target.dataset.zPart === "scroll") {
      const value = Number(event.detail?.scrollVal ?? event.detail?.value);
      if (Number.isFinite(value)) {
        const content = this._surfaceParts.content;
        content.scrollTop = (Math.min(45, Math.max(-45, value)) + 45) / 90 * Math.max(0, content.scrollHeight - content.clientHeight);
      }
      event.stopPropagation();
      return;
    }
    if (event.target.parentElement !== this || event.target.dataset.zPart !== "knob") return;
    const qty = event.detail?.qty ?? event.detail?.value;
    if (!Number.isFinite(Number(qty))) return;
    setAttribute(this, "qty", Number(qty));
    event.stopImmediatePropagation();
    this.dispatchEvent(new CustomEvent(event.type, {
      detail: { ...event.detail, qty: Number(qty), value: Number(qty) },
      bubbles: true,
      composed: true
    }));
  }
  _observe() {
    if (!this.isConnected) return;
    this._surfaceObserver ||= new MutationObserver((records) => {
      const holders = [
        this,
        this._surfaceParts?.extensions,
        this._surfaceParts?.content,
        this._surfaceParts?.image,
        this._surfaceParts?.media
      ];
      if (records.some((record) => record.type === "attributes" || holders.includes(record.target) && record.addedNodes.length || this._surfaceParts?.content.contains(record.target))) this._queueRefresh();
    });
    this._surfaceObserver.observe(this, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["slot"] });
    if (this.surfaceType === "view") {
      this._surfaceResizeObserver ||= new ResizeObserver(() => this._syncScroll());
      this._surfaceResizeObserver.disconnect();
      this._surfaceResizeObserver.observe(this._surfaceParts.content);
      for (const child of this._surfaceParts.content.children) this._surfaceResizeObserver.observe(child);
    }
  }
  _queueRefresh() {
    if (this._surfaceQueued) return;
    this._surfaceQueued = true;
    queueMicrotask(() => {
      this._surfaceQueued = false;
      this.refresh();
    });
  }
};
for (const [property, attribute] of Object.entries(properties)) {
  Object.defineProperty(ZSurface.prototype, property, {
    configurable: true,
    get() {
      if (booleanAttributes.has(attribute)) return this.hasAttribute(attribute);
      if (own(defaults, attribute)) return numberAttribute(
        this,
        attribute,
        attribute === "distance" && this.surfaceType === "view" ? 0 : defaults[attribute]
      );
      if (attribute === "size") return normaliseSize(this.getAttribute(attribute), this.defaultSize);
      return this.getAttribute(attribute) ?? "";
    },
    set(value) {
      if (booleanAttributes.has(attribute)) this.toggleAttribute(attribute, Boolean(value));
      else if (value === null || value === void 0) this.removeAttribute(attribute);
      else setAttribute(this, attribute, value);
    }
  });
}
function hydrateSurfaces(root) {
  if (!root) return;
  if (root instanceof ZSurface) root.refresh();
  for (const surface of root.querySelectorAll?.("z-view, z-spot") || []) {
    if (surface instanceof ZSurface) surface.refresh();
  }
}

// src/core.js
var THEMES = Object.freeze(["white", "light-blue", "black", "purple", "orange", "yellow", "blue", "green", "red", "gray"]);
var MODES = Object.freeze(["light", "light-filled", "dark", "dark-filled"]);
var instances = /* @__PURE__ */ new WeakMap();
var sequence = 0;
function choice(value, allowed, label) {
  if (!allowed.includes(value)) throw new TypeError(`Zircle: unknown ${label} "${value}". Use ${allowed.join(", ")}.`);
  return value;
}
async function createZircle(options = {}) {
  if (typeof document === "undefined") throw new Error("Zircle: createZircle() needs a browser. Call it after mounting your component.");
  if (options.signal?.aborted) throw new DOMException("Zircle initialization was cancelled.", "AbortError");
  const mount = typeof options.mount === "string" ? document.querySelector(options.mount) : options.mount;
  if (!(mount instanceof HTMLElement) || !mount.isConnected) throw new Error("Zircle: mount must be a connected HTML element or a matching selector.");
  if (mount.getRootNode() !== document) throw new Error("Zircle: Zumly requires a mount in the document light DOM.");
  if (instances.has(mount)) throw new Error("Zircle: this element already has an instance. Destroy it before mounting again.");
  const views = options.views;
  if (!views || typeof views !== "object" || !Object.keys(views).length) throw new TypeError("Zircle: provide a nonempty views map.");
  const initialView = options.initialView ?? Object.keys(views)[0];
  if (!Object.hasOwn(views, initialView)) throw new Error(`Zircle: initial view "${initialView}" is not registered.`);
  let theme = choice(options.theme ?? "black", THEMES, "theme");
  let mode = choice(options.mode ?? "dark", MODES, "mode");
  let shape = choice(options.shape ?? "circle", ["circle", "square"], "shape");
  let destroyed = false;
  let abortListener;
  const stage = document.createElement("div");
  stage.className = "zircle z-stage";
  const canvas = document.createElement("div");
  canvas.className = "zircle zumly-canvas";
  canvas.dataset.zircleInstance = String(++sequence);
  canvas.setAttribute("aria-label", options.label ?? "Zircle");
  const applyStyle = () => {
    canvas.dataset.theme = theme;
    canvas.dataset.mode = mode;
    canvas.dataset.shape = shape;
    stage.dataset.theme = theme;
    stage.dataset.mode = mode;
    stage.dataset.shape = shape;
  };
  applyStyle();
  const originalPosition = mount.style.position;
  const needsPosition = getComputedStyle(mount).position === "static";
  if (needsPosition) mount.style.position = "relative";
  stage.append(canvas);
  mount.append(stage);
  const resize = () => {
    const { width, height } = mount.getBoundingClientRect();
    const side = Math.max(1, Math.min(840, width, height));
    canvas.style.width = `${side}px`;
    canvas.style.height = `${side}px`;
    canvas.style.left = `${(width - side) / 2}px`;
    canvas.style.top = `${(height - side) / 2}px`;
    const diameter = side * 0.5;
    for (const [size, ratio] of Object.entries({ xxl: 1, xl: 260 / 420, l: 160 / 420, m: 100 / 420, s: 62 / 420, xs: 38 / 420, xxs: 16 / 420 })) {
      canvas.style.setProperty(`--z-size-${size}`, `${diameter * ratio}px`);
    }
    Orbit.refresh(canvas);
  };
  resize();
  const observer = new ResizeObserver(resize);
  observer.observe(mount);
  const app = new Zumly({
    mount: `[data-zircle-instance="${sequence}"]`,
    initialView,
    views,
    transitions: { driver: "css", duration: "700ms", ease: "ease-in-out", cover: "width", effects: ["blur(1px) opacity(0.35)", "blur(3px) opacity(0.12)"], hideTrigger: false, ...options.transitions },
    depthNav: false,
    lateralNav: false,
    inputs: { click: true, touch: true, keyboard: true, wheel: false, ...options.inputs },
    debug: options.debug ?? false,
    componentContext: options.context ?? /* @__PURE__ */ new Map(),
    preload: options.preload,
    deferred: false
  });
  const events = new EventTarget();
  const emit2 = (type, detail) => {
    events.dispatchEvent(new CustomEvent(type, { detail }));
    mount.dispatchEvent(new CustomEvent(`zircle:${type}`, { detail, bubbles: true }));
  };
  const refresh = () => {
    hydrateSurfaces(canvas);
    resize();
  };
  app.on("viewMounted", ({ node, viewName }) => {
    hydrateSurfaces(node);
    Orbit.refresh(node);
    if (node.classList.contains("is-current-view") && !node.style.transform) {
      node.style.transform = `translate(${(canvas.clientWidth - node.offsetWidth) / 2}px, ${(canvas.clientHeight - node.offsetHeight) / 2}px)`;
    }
    emit2("viewmount", { view: viewName, node });
  });
  const back = document.createElement("button");
  back.type = "button";
  back.className = "z-back z-depth-nav";
  back.textContent = "\u2190";
  back.setAttribute("aria-label", options.backLabel ?? "Go back");
  back.hidden = true;
  back.addEventListener("click", () => api.back());
  const notifyNavigation = () => {
    back.hidden = app.zoomLevel() <= 1;
    if (options.backButton !== false) canvas.append(back);
    emit2("viewchange", { view: app.getCurrentViewName(), depth: Math.max(0, app.zoomLevel() - 1) });
  };
  for (const event of ["afterZoomIn", "afterZoomOut", "afterLateral"]) app.on(event, notifyNavigation);
  if (options.router) app.use(ZumlyRouter, typeof options.router === "object" ? options.router : void 0);
  const validateTarget = (name) => {
    if (destroyed) throw new Error("Zircle: this instance has been destroyed.");
    if (!Object.hasOwn(views, name)) throw new Error(`Zircle: view "${name}" is not registered.`);
  };
  const api = {
    app,
    canvas,
    mount,
    getCurrentViewName: () => app.getCurrentViewName(),
    getHistory: () => app.storedViews.map((stage2) => stage2.views[0].viewName),
    getHistoryLength: () => app.storedViews.length,
    getTheme: () => theme,
    getMode: () => mode,
    getShape: () => shape,
    setTheme(value) {
      theme = choice(value, THEMES, "theme");
      applyStyle();
      emit2("stylechange", { theme, mode, shape });
      return api;
    },
    setMode(value) {
      mode = choice(value, MODES, "mode");
      applyStyle();
      emit2("stylechange", { theme, mode, shape });
      return api;
    },
    setShape(value) {
      shape = choice(value, ["circle", "square"], "shape");
      applyStyle();
      hydrateSurfaces(canvas);
      emit2("stylechange", { theme, mode, shape });
      return api;
    },
    async goTo(name, opts = {}) {
      validateTarget(name);
      await app.goTo(name, opts);
    },
    async zoomTo(name, opts = {}) {
      validateTarget(name);
      await app.zoomTo(name, opts);
    },
    async setView(target, opts = {}) {
      const name = typeof target === "string" ? target : target?.name;
      validateTarget(name);
      await app.goTo(name, { ...opts, props: typeof target === "object" ? target.params ?? opts.props : opts.props });
    },
    async back() {
      if (!destroyed) await app.back();
    },
    async zoomOut() {
      if (!destroyed) await app.zoomOut();
    },
    goBack() {
      return api.back();
    },
    refresh,
    on(type, handler) {
      events.addEventListener(type, handler);
      return () => events.removeEventListener(type, handler);
    },
    destroy() {
      if (destroyed) return;
      destroyed = true;
      if (abortListener) options.signal.removeEventListener("abort", abortListener);
      observer.disconnect();
      app.destroy();
      stage.remove();
      if (needsPosition && mount.style.position === "relative") mount.style.position = originalPosition;
      instances.delete(mount);
      emit2("destroy", {});
    }
  };
  instances.set(mount, api);
  if (options.signal) {
    abortListener = () => api.destroy();
    options.signal.addEventListener("abort", abortListener, { once: true });
  }
  try {
    await app.init();
    if (options.signal?.aborted) throw new DOMException("Zircle initialization was cancelled.", "AbortError");
    if (!app.isValid || !app.getCurrentViewName()) throw new Error(`Zircle: could not render initial view "${initialView}".`);
    refresh();
    notifyNavigation();
    emit2("ready", { instance: api });
    return api;
  } catch (error) {
    api.destroy();
    throw error;
  }
}

// src/components/z-canvas.js
var HTMLElementBase2 = globalThis.HTMLElement ?? class {
};
var ZCanvas = class extends HTMLElementBase2 {
  static observedAttributes = ["theme", "mode", "shape"];
  connectedCallback() {
    this._upgradeProperty("views");
    this._upgradeProperty("options");
    queueMicrotask(() => {
      if (this.isConnected && !this._ready) this.init();
    });
  }
  disconnectedCallback() {
    queueMicrotask(() => {
      if (!this.isConnected) this.destroy();
    });
  }
  _upgradeProperty(name) {
    if (Object.hasOwn(this, name)) {
      const value = this[name];
      delete this[name];
      this[name] = value;
    }
  }
  get ready() {
    return this._ready ?? this.init();
  }
  get instance() {
    return this._instance ?? null;
  }
  get views() {
    return this._views;
  }
  set views(value) {
    this._views = value;
  }
  get options() {
    return this._options;
  }
  set options(value) {
    this._options = value;
  }
  init() {
    if (this._ready) return this._ready;
    const generation = this._generation = (this._generation ?? 0) + 1;
    this._controller = new AbortController();
    const signal = this._controller.signal;
    const views = { ...this._views };
    let templateError;
    for (const template of this.querySelectorAll(":scope > template[data-view]")) {
      const name = template.dataset.view;
      if (Object.hasOwn(views, name)) {
        templateError = new Error(`Zircle: duplicate view "${name}".`);
        break;
      }
      if (!name.trim() || template.content.children.length !== 1) {
        templateError = new Error("Zircle: each view template needs a nonempty data-view name and exactly one root element.");
        break;
      }
      views[name] = () => template.content.firstElementChild.cloneNode(true);
    }
    this._ready = (templateError ? Promise.reject(templateError) : createZircle({
      ...this._options,
      mount: this,
      views,
      signal,
      initialView: this.getAttribute("initial-view") ?? this._options?.initialView ?? Object.keys(views)[0],
      theme: this.getAttribute("theme") ?? this._options?.theme,
      mode: this.getAttribute("mode") ?? this._options?.mode,
      shape: this.getAttribute("shape") ?? this._options?.shape,
      label: this.getAttribute("aria-label") ?? this._options?.label,
      router: this.hasAttribute("router") || this._options?.router
    })).then((instance) => {
      if (generation !== this._generation || !this.isConnected) {
        instance.destroy();
        return null;
      }
      this._instance = instance;
      for (const name of ["theme", "mode", "shape"]) this._applyStyleAttribute(name);
      this.setAttribute("data-ready", "");
      return instance;
    }).catch((error) => {
      if (signal.aborted) return null;
      throw error;
    });
    this._ready.catch((error) => this.dispatchEvent(new CustomEvent("zircle:error", { detail: { error }, bubbles: true })));
    return this._ready;
  }
  attributeChangedCallback(name, oldValue, value) {
    if (oldValue === value || !this._instance) return;
    this._applyStyleAttribute(name);
  }
  _applyStyleAttribute(name) {
    const defaults2 = { theme: "black", mode: "dark", shape: "circle" };
    const value = this.getAttribute(name) ?? this._options?.[name] ?? defaults2[name];
    const suffix = `${name[0].toUpperCase()}${name.slice(1)}`;
    if (this._instance[`get${suffix}`]() !== value) this._instance[`set${suffix}`](value);
  }
  async setView(name, options) {
    return (await this.ready)?.setView(name, options);
  }
  async back() {
    return (await this.ready)?.back();
  }
  destroy() {
    this._generation = (this._generation ?? 0) + 1;
    this._controller?.abort();
    this._controller = null;
    this._instance?.destroy();
    this._instance = null;
    this._ready = null;
    this.removeAttribute("data-ready");
  }
};

// src/components/z-view.js
var ZView = class extends ZSurface {
  get defaultSize() {
    return "xxl";
  }
  get surfaceType() {
    return "view";
  }
};

// src/components/z-spot.js
var ZSpot = class extends ZSurface {
  get defaultSize() {
    return "m";
  }
  get surfaceType() {
    return "spot";
  }
};

// src/components/control-utils.js
var HTMLElementBase3 = globalThis.HTMLElement ?? class {
};
var FULL_RING_RANGE = "359.99deg";
function isViewDefinition(element) {
  const view = element.closest("z-view");
  return view?.hasAttribute("name") && !view.hasAttribute("data-view-name") && view.parentElement?.localName === "z-canvas";
}
function numberAttribute2(element, name, fallback) {
  const raw = element.getAttribute(name);
  const value = raw === null || raw.trim() === "" ? NaN : Number(raw);
  return Number.isFinite(value) ? value : fallback;
}
function reflectNumber(element, name, value) {
  if (value === null || value === void 0) element.removeAttribute(name);
  else if (Number.isFinite(Number(value))) element.setAttribute(name, String(value));
}
function emit(element, type, detail, options = {}) {
  return element.dispatchEvent(new element.ownerDocument.defaultView.CustomEvent(
    type,
    { bubbles: true, composed: true, detail, ...options }
  ));
}
function upgradeProperties(element, names) {
  for (const name of names) {
    if (!Object.prototype.hasOwnProperty.call(element, name)) continue;
    const value = element[name];
    delete element[name];
    element[name] = value;
  }
}
function createRing(element, withHandle = false) {
  const document2 = element.ownerDocument;
  element.classList.add("gravity-spot");
  const orbit = Array.from(element.children).find((node) => node.classList.contains("z-control-orbit")) ?? document2.createElement("div");
  orbit.className = "orbit-12 z-control-orbit";
  orbit.setAttribute("aria-hidden", "true");
  const progress = orbit.querySelector("o-progress") ?? document2.createElement("o-progress");
  progress.className = "z-control-progress";
  progress.setAttribute("variant", "stroke");
  progress.setAttribute("max", "100");
  orbit.append(progress);
  let handle;
  if (withHandle) {
    handle = orbit.querySelector(".z-control-handle") ?? document2.createElement("span");
    handle.className = "satellite z-control-handle";
    handle.style.setProperty("--o-from", "0deg");
    handle.style.setProperty("--o-angle", "0deg");
    orbit.append(handle);
  }
  element.append(orbit);
  return { orbit, progress, handle };
}
var RadialControl = class extends HTMLElementBase3 {
  get step() {
    const value = numberAttribute2(this, "step", 1);
    return value > 0 ? value : 1;
  }
  set step(value) {
    reflectNumber(this, "step", value);
  }
  get unit() {
    return this.getAttribute("unit") ?? "";
  }
  set unit(value) {
    this.setAttribute("unit", value ?? "");
  }
  get disabled() {
    return this.hasAttribute("disabled");
  }
  set disabled(value) {
    this.toggleAttribute("disabled", Boolean(value));
  }
  get value() {
    return this.normalize(numberAttribute2(this, this.valueAttribute, this.min));
  }
  set value(value) {
    reflectNumber(this, this.valueAttribute, this.normalize(Number(value)));
  }
  normalize(value) {
    if (!Number.isFinite(value)) return this.min;
    const bounded = Math.min(this.max, Math.max(this.min, value));
    const stepped = this.min + Math.round((bounded - this.min) / this.step) * this.step;
    return Math.min(this.max, Math.max(this.min, Number(stepped.toFixed(10))));
  }
  upgradeProperties(names) {
    upgradeProperties(this, names);
  }
  connectedCallback() {
    if (isViewDefinition(this)) return;
    if (this._listeners) return;
    this._ring ??= createRing(this, true);
    this._ring.progress.toggleAttribute("interactive", this.controlKind === "scroll");
    if (this.controlKind === "knob" && !this._valueLabel) {
      this._valueLabel = this.querySelector(":scope > .z-knob-value") ?? this.ownerDocument.createElement("span");
      this._valueLabel.className = "z-knob-value";
      this._valueLabel.setAttribute("aria-hidden", "true");
      this._numberLabel = this._valueLabel.querySelector(".z-knob-number") ?? this.ownerDocument.createElement("span");
      this._numberLabel.className = "z-knob-number";
      this._unitLabel = this._valueLabel.querySelector(".z-knob-unit") ?? this.ownerDocument.createElement("span");
      this._unitLabel.className = "z-knob-unit";
      this._valueLabel.append(this._numberLabel, this._unitLabel);
      this.append(this._valueLabel);
    }
    this.classList.add(`z-${this.controlKind}`);
    this.setAttribute("role", "slider");
    if (!this.hasAttribute("aria-label") && !this.hasAttribute("aria-labelledby")) this.setAttribute("aria-label", this.defaultLabel);
    this._listeners = new this.ownerDocument.defaultView.AbortController();
    const options = { signal: this._listeners.signal };
    this.addEventListener("keydown", (event) => this._onKey(event), options);
    this.addEventListener("pointerdown", (event) => this._onPointerDown(event), options);
    this.ownerDocument.addEventListener("pointermove", (event) => this._onPointerMove(event), options);
    this.ownerDocument.addEventListener("pointerup", (event) => this._onPointerEnd(event), options);
    this.ownerDocument.addEventListener("pointercancel", (event) => this._onPointerEnd(event), options);
    this.addEventListener("click", (event) => event.stopPropagation(), options);
    this._render();
  }
  disconnectedCallback() {
    this._releasePointer();
    this._listeners?.abort();
    this._listeners = null;
  }
  attributeChangedCallback() {
    this._render();
  }
  _render() {
    if (!this._ring) return;
    const span = this.max - this.min;
    const progress = span ? (this.value - this.min) / span * 100 : 0;
    const angle = this.startAngle + progress / 100 * this.angleRange;
    this.style.setProperty(`--z-${this.controlKind}-angle`, `${angle}deg`);
    this._ring.progress.setAttribute("value", String(this.controlKind === "scroll" ? 100 : progress));
    this._ring.progress.style.setProperty("--o-range", this.angleRange === 360 ? FULL_RING_RANGE : `${this.angleRange}deg`);
    this._ring.progress.style.setProperty("--o-from", `${this.startAngle + 90}deg`);
    this._ring.handle.style.setProperty("--o-offset", `${angle}deg`);
    if (this._valueLabel) {
      this._numberLabel.textContent = String(this.value);
      this._unitLabel.textContent = this.unit;
      this._unitLabel.hidden = !this.unit;
    }
    this.setAttribute("aria-valuemin", String(this.min));
    this.setAttribute("aria-valuemax", String(this.max));
    this.setAttribute("aria-valuenow", String(this.value));
    this.setAttribute("aria-valuetext", `${this.value}${this.unit ? ` ${this.unit}` : ""}`);
    this.setAttribute("aria-disabled", String(this.disabled));
    if (this.disabled) {
      if (this.tabIndex >= 0) this._enabledTabIndex = this.tabIndex;
      this.tabIndex = -1;
      this._releasePointer();
    } else if (!this.hasAttribute("tabindex") || this.tabIndex === -1) this.tabIndex = this._enabledTabIndex ?? 0;
  }
  valueFromPointer(event) {
    const bounds = this.getBoundingClientRect();
    const angle = (Math.atan2(
      event.clientY - bounds.top - bounds.height / 2,
      event.clientX - bounds.left - bounds.width / 2
    ) * 180 / Math.PI + 360) % 360;
    return this.min + angle / 360 * (this.max - this.min);
  }
  _input(value) {
    const previous = this.value;
    this.value = value;
    if (this.value === previous) return false;
    emit(this, "input", this.eventDetail);
    return true;
  }
  _onKey(event) {
    if (this.disabled || event.altKey || event.ctrlKey || event.metaKey) return;
    const values = {
      ArrowUp: this.value + this.step,
      ArrowRight: this.value + this.step,
      ArrowDown: this.value - this.step,
      ArrowLeft: this.value - this.step,
      PageUp: this.value + this.step * 10,
      PageDown: this.value - this.step * 10,
      Home: this.min,
      End: this.max
    };
    if (!(event.key in values)) return;
    event.preventDefault();
    event.stopPropagation();
    if (this._input(values[event.key])) emit(this, "change", this.eventDetail);
  }
  _onPointerDown(event) {
    if (this.disabled || event.button !== 0 || this._pointerId !== void 0) return;
    event.preventDefault();
    event.stopPropagation();
    this.focus({ preventScroll: true });
    this._pointerId = event.pointerId;
    this._pointerStart = this.value;
    try {
      this.setPointerCapture(event.pointerId);
    } catch {
    }
    this._input(this.valueFromPointer(event));
  }
  _onPointerMove(event) {
    if (this._pointerId === void 0 || event.pointerId !== this._pointerId) return;
    event.preventDefault();
    this._input(this.valueFromPointer(event));
  }
  _onPointerEnd(event) {
    if (this._pointerId === void 0 || event.pointerId !== this._pointerId) return;
    const changed = this.value !== this._pointerStart;
    this._releasePointer();
    if (changed) emit(this, "change", this.eventDetail);
  }
  _releasePointer() {
    if (this._pointerId === void 0) return;
    try {
      this.releasePointerCapture(this._pointerId);
    } catch {
    }
    this._pointerId = void 0;
  }
};

// src/components/z-list.js
var ZList = class extends HTMLElementBase3 {
  static observedAttributes = ["per-page", "page", "size", "square"];
  get perPage() {
    return Math.max(1, Math.floor(numberAttribute2(this, "per-page", 5)));
  }
  set perPage(value) {
    reflectNumber(this, "per-page", value);
  }
  get page() {
    return Math.max(1, Math.min(this.pageCount || 1, Math.floor(numberAttribute2(this, "page", 1))));
  }
  set page(value) {
    reflectNumber(this, "page", value);
  }
  get pageCount() {
    return Math.ceil(this._spots().length / this.perPage);
  }
  get size() {
    return normaliseSize(this.getAttribute("size"), "xxl");
  }
  set size(value) {
    this.setAttribute("size", value);
  }
  get square() {
    return this.hasAttribute("square");
  }
  set square(value) {
    this.toggleAttribute("square", Boolean(value));
  }
  get items() {
    return this._items?.slice() ?? [];
  }
  set items(value) {
    if (!Array.isArray(value)) throw new TypeError("z-list.items must be an array.");
    this._items = value.slice();
    this._itemsDirty = true;
    if (this.isConnected && this._pager) this._render();
  }
  get renderItem() {
    return this._renderItem;
  }
  set renderItem(value) {
    if (value != null && typeof value !== "function") throw new TypeError("z-list.renderItem must be a function.");
    this._renderItem = value;
    this._itemsDirty = true;
    if (this.isConnected && this._pager) this._render();
  }
  connectedCallback() {
    if (isViewDefinition(this)) return;
    upgradeProperties(this, ["items", "renderItem", "page", "perPage", "size", "square"]);
    this.classList.add("z-list", "gravity-spot");
    if (!this._orbit) {
      this._orbit = this.querySelector(":scope > .z-list-orbit") ?? this.ownerDocument.createElement("div");
      this._orbit.className = "orbit-12 z-list-orbit";
      this._orbit.style.setProperty("--o-range", "0deg");
      this._pager = this.querySelector(":scope > .z-list-pagination") ?? this.ownerDocument.createElement("nav");
      this._pager.className = "z-list-pagination orbit-12";
      this._pager.style.setProperty("--o-range", "0deg");
      this._pager.setAttribute("aria-label", "List pages");
      this.append(this._orbit, this._pager);
    }
    if (!this._listeners) {
      this._listeners = new this.ownerDocument.defaultView.AbortController();
      this._pager.addEventListener("click", (event) => {
        const button = event.target.closest("button[data-page]");
        if (!button || button.disabled || !this._pager.contains(button)) return;
        event.stopPropagation();
        this.page = Number(button.dataset.page);
      }, { signal: this._listeners.signal });
    }
    this._observer ??= new this.ownerDocument.defaultView.MutationObserver(() => this._render());
    this._render();
  }
  disconnectedCallback() {
    this._observer?.disconnect();
    this._listeners?.abort();
    this._listeners = null;
  }
  attributeChangedCallback() {
    if (this.isConnected && this._pager) this._render();
  }
  next() {
    this.page += 1;
    return this.page;
  }
  previous() {
    this.page -= 1;
    return this.page;
  }
  _spots() {
    const direct = Array.from(this.children).filter((child) => child.localName === "z-spot");
    return this._orbit ? [...this._orbit.children, ...direct].filter((child) => child.localName === "z-spot") : direct;
  }
  _renderItems() {
    if (!this._itemsDirty || !this._items) return;
    const nodes = this._items.map((item, index) => {
      const rendered = this._renderItem ? this._renderItem(item, index) : typeof item === "object" && item !== null ? item.label ?? item.name ?? String(item) : String(item);
      if (rendered?.nodeType === 1 && rendered.localName === "z-spot") return rendered;
      const spot = this.ownerDocument.createElement("z-spot");
      if (rendered?.nodeType) spot.append(rendered);
      else spot.textContent = rendered == null ? "" : String(rendered);
      return spot;
    });
    for (const node of this._generated ?? []) if (!nodes.includes(node)) node.remove();
    this._generated = nodes;
    this._orbit.append(...nodes);
    this._itemsDirty = false;
  }
  _renderPager(count, page) {
    const first = count > 5 ? Math.max(1, Math.min(page - 2, count - 4)) : 1;
    const pages = Array.from({ length: Math.min(5, count) }, (_, index) => ({
      kind: "page",
      page: first + index,
      text: String(first + index),
      label: `Page ${first + index}`
    }));
    if (count > 5) {
      pages.unshift({ kind: "previous", page: Math.max(1, page - 1), text: "\u2039", label: "Previous page", disabled: page === 1 });
      pages.push({ kind: "next", page: Math.min(count, page + 1), text: "\u203A", label: "Next page", disabled: page === count });
    }
    const signature = pages.map((item) => item.kind === "page" ? item.page : item.kind).join(",");
    let restoreFocus;
    if (signature !== this._pagerSignature) {
      const active = this.ownerDocument.activeElement;
      if (active?.parentElement === this._pager) restoreFocus = { kind: active.dataset.kind, page: active.dataset.page };
      const fragment = this.ownerDocument.createDocumentFragment();
      for (const item of pages) {
        const button = this.ownerDocument.createElement("button");
        button.className = `z-list-page satellite${item.kind === "page" ? "" : ` z-list-${item.kind}`}`;
        button.type = "button";
        fragment.append(button);
      }
      this._pager.replaceChildren(fragment);
      this._pagerSignature = signature;
    }
    this._pager.hidden = count < 2;
    [...this._pager.children].forEach((button, index) => {
      const item = pages[index];
      button.dataset.page = String(item.page);
      button.dataset.kind = item.kind;
      button.textContent = item.text;
      button.setAttribute("aria-label", item.label);
      button.disabled = Boolean(item.disabled);
      button.style.setProperty("--o-offset", `${90 + (pages.length - 1) * 11 - index * 22}deg`);
      button.style.setProperty("--o-from", "0deg");
      button.style.setProperty("--o-angle", "0deg");
      if (item.kind === "page" && item.page === page) button.setAttribute("aria-current", "page");
      else button.removeAttribute("aria-current");
    });
    if (restoreFocus && count > 1) {
      const buttons = [...this._pager.children];
      const target = buttons.find((button) => button.dataset.kind === restoreFocus.kind && (restoreFocus.kind !== "page" || button.dataset.page === restoreFocus.page)) ?? buttons.find((button) => button.getAttribute("aria-current") === "page");
      target?.focus({ preventScroll: true });
    }
  }
  _render() {
    if (!this._pager || this._rendering) return;
    this._rendering = true;
    this._observer?.disconnect();
    try {
      this._renderItems();
      for (const child of Array.from(this.children)) if (child.localName === "z-spot") this._orbit.append(child);
      const spots = this._spots();
      const page = this.page;
      const count = this.pageCount;
      const first = (page - 1) * this.perPage;
      const visible = spots.slice(first, first + this.perPage);
      this._originalDistances ??= /* @__PURE__ */ new WeakMap();
      this._originalAlignments ??= /* @__PURE__ */ new WeakMap();
      for (const spot of this._managedSpots ?? []) {
        if (spots.includes(spot)) continue;
        const alignment = this._originalAlignments.get(spot);
        if (alignment) spot.style.setProperty("--o-aligment", alignment);
        else spot.style.removeProperty("--o-aligment");
        spot.classList.remove("at-center");
        spot.hidden = false;
      }
      this._managedSpots = spots;
      spots.forEach((spot) => {
        spot.hidden = !visible.includes(spot);
        if (!this._originalDistances.has(spot)) {
          const original = spot.hasAttribute("data-z-list-distance") ? spot.getAttribute("data-z-list-distance") || null : spot.getAttribute("distance");
          this._originalDistances.set(spot, original);
          spot.setAttribute("data-z-list-distance", original ?? "");
        }
        if (!this._originalAlignments.has(spot)) this._originalAlignments.set(spot, spot.style.getPropertyValue("--o-aligment"));
      });
      visible.forEach((spot, index) => {
        spot.setAttribute("angle", String(360 / visible.length * index - 90));
        spot.classList.toggle("at-center", visible.length === 1);
        const distance = visible.length === 1 ? "0" : this._originalDistances.get(spot) ?? "100";
        spot.setAttribute("distance", distance);
        spot.style.setProperty("--o-aligment", `calc(var(--o-radius) * ${1 - Math.max(0, numberAttribute2(spot, "distance", 100)) / 100})`);
      });
      this.style.setProperty("--z-list-size", `var(--z-size-${this.size}, var(--z-size-xxl))`);
      this.classList.toggle("is-square", this.hasAttribute("square"));
      this._renderPager(count, page);
      const previousPage = this._lastPage;
      this._lastPage = page;
      if (this.hasAttribute("page") && this.getAttribute("page") !== String(page)) this.setAttribute("page", String(page));
      if (previousPage !== void 0 && previousPage !== page) emit(this, "pagechange", { page, pageCount: count, previousPage });
    } finally {
      this._rendering = false;
      if (this.isConnected) {
        this._observer?.observe(this, { childList: true });
        this._observer?.observe(this._orbit, { childList: true });
      }
    }
  }
};

// src/components/z-dialog.js
var ZDialog = class extends HTMLElementBase3 {
  static observedAttributes = ["open", "visible", "duration", "self-close", "size", "square", "circle", "image-path", "aria-label", "aria-labelledby", "aria-describedby"];
  get open() {
    return this.hasAttribute("open") || this.hasAttribute("visible");
  }
  set open(value) {
    if (value) this.show();
    else this.close();
  }
  get visible() {
    return this.open;
  }
  set visible(value) {
    this.open = value;
  }
  get duration() {
    return Math.max(0, numberAttribute2(this, "duration", this.selfClose ? 1e4 : 0));
  }
  set duration(value) {
    reflectNumber(this, "duration", value);
  }
  get selfClose() {
    return this.hasAttribute("self-close");
  }
  set selfClose(value) {
    this.toggleAttribute("self-close", Boolean(value));
  }
  get size() {
    return normaliseSize(this.getAttribute("size"), "xxl");
  }
  set size(value) {
    this.setAttribute("size", value);
  }
  get square() {
    return this.hasAttribute("square");
  }
  set square(value) {
    this.toggleAttribute("square", Boolean(value));
  }
  get circle() {
    return this.hasAttribute("circle");
  }
  set circle(value) {
    this.toggleAttribute("circle", Boolean(value));
  }
  get imagePath() {
    return this.getAttribute("image-path") ?? "";
  }
  set imagePath(value) {
    if (value) this.setAttribute("image-path", value);
    else this.removeAttribute("image-path");
  }
  get returnValue() {
    return this._panel?.returnValue ?? "";
  }
  connectedCallback() {
    if (isViewDefinition(this)) return;
    upgradeProperties(this, ["open", "visible", "duration", "selfClose", "size", "imagePath", "square", "circle"]);
    this.classList.add("z-dialog");
    if (!this._panel) this._build();
    if (!this._listeners) {
      this._listeners = new this.ownerDocument.defaultView.AbortController();
      const options = { signal: this._listeners.signal };
      this._closeButton.addEventListener("click", () => this.close("", "button"), options);
      this._panel.addEventListener("cancel", (event) => {
        event.preventDefault();
        if (emit(this, "cancel", {}, { cancelable: true })) this.close("", "escape");
      }, options);
      this._panel.addEventListener("close", () => {
        if (this._visible && !this._panel.open) this.close(this._panel.returnValue, "form");
      }, options);
      this._panel.addEventListener("click", (event) => {
        event.stopPropagation();
        if (event.target !== this._panel) return;
        const rect = this._panel.getBoundingClientRect();
        if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) this.close("", "backdrop");
      }, options);
      this._panel.addEventListener("keydown", (event) => this._fallbackKeys(event), options);
      this._content.addEventListener("scroll", () => this._measureScroll(), { ...options, passive: true });
      this._scroll.addEventListener("input", (event) => {
        event.stopPropagation();
        const span = this._content.scrollHeight - this._content.clientHeight;
        if (span > 0) this._content.scrollTop = (event.detail.scrollVal + 45) / 90 * span;
      }, options);
    }
    this._observer ??= new this.ownerDocument.defaultView.MutationObserver(() => this._adoptContent());
    const Resize = this.ownerDocument.defaultView.ResizeObserver;
    if (Resize) this._resize ??= new Resize(() => this._measureScroll());
    this._adoptContent();
    this._observer.observe(this, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["slot"] });
    this._render();
  }
  disconnectedCallback() {
    this._stopTimer();
    this._observer?.disconnect();
    this._resize?.disconnect();
    this._listeners?.abort();
    this._listeners = null;
    if (this._panel?.open) this._panel.close?.();
    this._panel?.removeAttribute("open");
    this._visible = false;
    this._restoreFocus();
  }
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue || !this._panel || this._syncing) return;
    this._render();
    if (this._visible && (name === "duration" || name === "self-close")) this._startTimer();
  }
  show() {
    this.hidden = false;
    this.setAttribute("open", "");
    if (this.isConnected) this._render();
    return this;
  }
  close(returnValue = "", reason = "api") {
    const wasOpen = this._visible;
    this._syncing = true;
    this.removeAttribute("open");
    this.removeAttribute("visible");
    this._syncing = false;
    this._stopTimer();
    this._visible = false;
    if (this._panel) {
      this._panel.returnValue = String(returnValue);
      if (this._panel.open && typeof this._panel.close === "function") this._panel.close(String(returnValue));
      this._panel.removeAttribute("open");
    }
    this._restoreFocus();
    if (wasOpen) emit(this, "close", { returnValue: String(returnValue), reason });
    return this;
  }
  _build() {
    const document2 = this.ownerDocument;
    this._panel = this.querySelector(":scope > .z-dialog-panel") ?? document2.createElement("dialog");
    this._panel.className = "z-dialog-panel";
    this._image = this._panel.querySelector(":scope > img.z-dialog-image");
    this._closeButton = this._panel.querySelector(":scope > .z-dialog-close") ?? document2.createElement("button");
    this._closeButton.type = "button";
    this._closeButton.className = "z-dialog-close";
    this._closeButton.setAttribute("aria-label", "Close dialog");
    this._closeButton.textContent = "\xD7";
    this._content = this._panel.querySelector(":scope > .z-dialog-content") ?? document2.createElement("div");
    this._content.className = "z-dialog-content";
    this._progress = this._panel.querySelector(":scope > .z-dialog-progress") ?? document2.createElement("z-slider");
    this._progress.className = "z-dialog-progress";
    this._progress.setAttribute("aria-label", "Time until dialog closes");
    this._progress.setAttribute("aria-hidden", "true");
    this._scroll = this._panel.querySelector(":scope > .z-dialog-scroll") ?? document2.createElement("z-scroll");
    this._scroll.className = "z-dialog-scroll";
    this._scroll.setAttribute("aria-label", "Scroll dialog content");
    this._scroll.hidden = true;
    this._imageSlot = this._panel.querySelector(":scope > .z-dialog-image-slot") ?? document2.createElement("div");
    this._imageSlot.className = "z-dialog-image-slot";
    this._media = this._panel.querySelector(":scope > .z-dialog-media") ?? document2.createElement("div");
    this._media.className = "z-dialog-media";
    this._extensions = this._panel.querySelector(":scope > .z-dialog-extensions") ?? document2.createElement("div");
    this._extensions.className = "z-dialog-extensions";
    this._panel.append(this._imageSlot, this._content, this._media, this._extensions, this._progress, this._scroll, this._closeButton);
    this.append(this._panel);
  }
  _adoptContent() {
    const nodes = [...this.childNodes, ...this._content.childNodes, ...this._imageSlot.childNodes, ...this._media.childNodes, ...this._extensions.childNodes];
    for (const child of nodes) {
      if (child === this._panel) continue;
      const slot = child.nodeType === 1 ? child.getAttribute("slot") : null;
      const target = slot === "image" ? this._imageSlot : slot === "media" ? this._media : slot === "extension" ? this._extensions : this._content;
      if (child.parentNode !== target) target.append(child);
    }
    this._imageSlot.hidden = !this._imageSlot.childNodes.length || Boolean(this.imagePath);
    this._media.hidden = !this._media.childNodes.length;
    this._extensions.hidden = !this._extensions.childNodes.length;
    this._resize?.disconnect();
    if (this.isConnected) {
      this._resize?.observe(this._content);
      for (const child of this._content.children) this._resize?.observe(child);
    }
    this._measureScroll();
  }
  _render() {
    if (!this._panel) return;
    this.style.setProperty("--z-dialog-size", `var(--z-size-${this.size}, var(--z-size-xxl))`);
    this._panel.classList.toggle("is-square", this.hasAttribute("square"));
    this._panel.classList.toggle("is-circle", !this.hasAttribute("square"));
    for (const name of ["aria-label", "aria-labelledby", "aria-describedby"]) {
      if (this.hasAttribute(name)) this._panel.setAttribute(name, this.getAttribute(name));
      else this._panel.removeAttribute(name);
    }
    if (!this._panel.hasAttribute("aria-label") && !this._panel.hasAttribute("aria-labelledby")) this._panel.setAttribute("aria-label", "Dialog");
    if (this.imagePath) {
      if (!this._image) {
        this._image = this.ownerDocument.createElement("img");
        this._image.className = "z-dialog-image";
        this._image.alt = "";
        this._panel.prepend(this._image);
      }
      this._image.src = this.imagePath;
    } else if (this._image) {
      this._image.remove();
      this._image = null;
    }
    this._imageSlot.hidden = !this._imageSlot.childNodes.length || Boolean(this.imagePath);
    this._progress.hidden = this.duration === 0;
    if (!this.isConnected) return;
    if (this.open && !this._visible) {
      this._returnFocus = this.ownerDocument.activeElement;
      this.hidden = false;
      this._visible = true;
      this._panel.returnValue = "";
      if (typeof this._panel.showModal === "function") {
        this._panel.removeAttribute("open");
        this._panel.showModal();
      } else {
        this._panel.setAttribute("open", "");
        this._panel.setAttribute("role", "dialog");
        this._panel.setAttribute("aria-modal", "true");
        this._closeButton.focus();
      }
      this._startTimer();
      this._measureScroll();
      emit(this, "open", {});
    } else if (!this.open && this._visible) this.close("", "attribute");
  }
  _measureScroll() {
    if (!this._scroll) return;
    const span = this._content.scrollHeight - this._content.clientHeight;
    this._scroll.hidden = !this._visible || span < 2 || this.hasAttribute("square");
    this._scroll.scrollVal = span > 0 ? -45 + this._content.scrollTop / span * 90 : -45;
  }
  _startTimer() {
    this._stopTimer();
    if (!this.duration || !this._visible) return;
    const window2 = this.ownerDocument.defaultView;
    const duration = this.duration;
    const start = window2.performance.now();
    this._progress.progress = 0;
    const tick = () => {
      const elapsed = window2.performance.now() - start;
      this._progress.progress = Math.min(100, elapsed / duration * 100);
      if (this._visible && elapsed < duration) this._timerFrame = window2.requestAnimationFrame(tick);
    };
    this._timerFrame = window2.requestAnimationFrame(tick);
    this._timer = window2.setTimeout(() => {
      this._progress.progress = 100;
      this.close("", "timeout");
      emit(this, "done", { reason: "timeout" });
    }, duration);
  }
  _stopTimer() {
    const window2 = this.ownerDocument.defaultView;
    window2.clearTimeout(this._timer);
    window2.cancelAnimationFrame(this._timerFrame);
    this._timer = this._timerFrame = null;
  }
  _restoreFocus() {
    if (this._returnFocus?.isConnected && typeof this._returnFocus.focus === "function") this._returnFocus.focus({ preventScroll: true });
    this._returnFocus = null;
  }
  _fallbackKeys(event) {
    if (typeof this._panel.showModal === "function") return;
    if (event.key === "Escape") {
      event.preventDefault();
      if (emit(this, "cancel", {}, { cancelable: true })) this.close("", "escape");
    }
    if (event.key !== "Tab") return;
    const focusable = [...this._panel.querySelectorAll("a[href],button,input,select,textarea,[tabindex]")].filter((element) => !element.disabled && element.tabIndex >= 0 && !element.hidden && element.getClientRects().length);
    const first = focusable[0] ?? this._closeButton;
    const last = focusable.at(-1) ?? this._closeButton;
    if (event.shiftKey && this.ownerDocument.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && this.ownerDocument.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  }
};

// src/components/z-knob.js
var ZKnob = class extends RadialControl {
  static observedAttributes = ["qty", "min", "max", "step", "unit", "disabled"];
  get controlKind() {
    return "knob";
  }
  get min() {
    return numberAttribute2(this, "min", 0);
  }
  set min(value) {
    reflectNumber(this, "min", value);
  }
  get max() {
    return Math.max(this.min, numberAttribute2(this, "max", 100));
  }
  set max(value) {
    reflectNumber(this, "max", value);
  }
  get qty() {
    return this.value;
  }
  set qty(value) {
    this.value = value;
  }
  get valueAttribute() {
    return "qty";
  }
  get startAngle() {
    return 0;
  }
  get angleRange() {
    return 360;
  }
  get defaultLabel() {
    return "Value";
  }
  get eventDetail() {
    return { value: this.value, qty: this.value };
  }
  connectedCallback() {
    this.upgradeProperties(["qty", "min", "max", "step", "unit", "value", "disabled"]);
    super.connectedCallback();
  }
};

// src/components/z-slider.js
var ZSlider = class extends HTMLElementBase3 {
  static observedAttributes = ["progress", "unit"];
  get progress() {
    return Math.max(0, Math.min(100, numberAttribute2(this, "progress", 0)));
  }
  set progress(value) {
    reflectNumber(this, "progress", value);
  }
  get value() {
    return this.progress;
  }
  set value(value) {
    this.progress = value;
  }
  get unit() {
    return this.getAttribute("unit") ?? "%";
  }
  set unit(value) {
    this.setAttribute("unit", value ?? "");
  }
  connectedCallback() {
    if (isViewDefinition(this)) return;
    upgradeProperties(this, ["progress", "value", "unit"]);
    this._ring ??= createRing(this);
    this.classList.add("z-slider");
    this.setAttribute("role", "progressbar");
    this.setAttribute("aria-valuemin", "0");
    this.setAttribute("aria-valuemax", "100");
    if (!this.hasAttribute("aria-label") && !this.hasAttribute("aria-labelledby")) this.setAttribute("aria-label", "Progress");
    this._render();
  }
  attributeChangedCallback() {
    this._render();
  }
  _render() {
    if (!this._ring) return;
    this._ring.progress.setAttribute("value", String(this.progress));
    this._ring.progress.style.setProperty("--o-range", FULL_RING_RANGE);
    this._ring.progress.style.setProperty("--o-from", "0deg");
    this.setAttribute("aria-valuenow", String(this.progress));
    this.setAttribute("aria-valuetext", `${this.progress}${this.unit}`);
  }
};

// src/components/z-scroll.js
var ZScroll = class extends RadialControl {
  static observedAttributes = ["scroll-val", "step", "unit", "disabled"];
  get controlKind() {
    return "scroll";
  }
  get min() {
    return -45;
  }
  get max() {
    return 45;
  }
  get scrollVal() {
    return this.value;
  }
  set scrollVal(value) {
    this.value = value;
  }
  get valueAttribute() {
    return "scroll-val";
  }
  get startAngle() {
    return -45;
  }
  get angleRange() {
    return 90;
  }
  get defaultLabel() {
    return "Scroll position";
  }
  get eventDetail() {
    return { value: this.value, scrollVal: this.value };
  }
  connectedCallback() {
    this.upgradeProperties(["scrollVal", "step", "unit", "value", "disabled"]);
    super.connectedCallback();
  }
  valueFromPointer(event) {
    const bounds = this.getBoundingClientRect();
    return this.normalize(Math.atan2(
      event.clientY - bounds.top - bounds.height / 2,
      event.clientX - bounds.left - bounds.width / 2
    ) * 180 / Math.PI);
  }
};

// src/components/z-pagination.js
var ZPagination = class extends HTMLElementBase3 {
  static observedAttributes = ["index", "active", "angle", "distance", "size", "disabled"];
  get index() {
    return Math.max(0, Math.floor(numberAttribute2(this, "index", 0)));
  }
  set index(value) {
    reflectNumber(this, "index", value);
  }
  get active() {
    return Math.max(0, Math.floor(numberAttribute2(this, "active", 0)));
  }
  set active(value) {
    reflectNumber(this, "active", value);
  }
  get angle() {
    return numberAttribute2(this, "angle", 0);
  }
  set angle(value) {
    reflectNumber(this, "angle", value);
  }
  get distance() {
    return Math.max(0, numberAttribute2(this, "distance", 100));
  }
  set distance(value) {
    reflectNumber(this, "distance", value);
  }
  get size() {
    return normaliseSize(this.getAttribute("size"), "xs");
  }
  set size(value) {
    this.setAttribute("size", value);
  }
  get disabled() {
    return this.hasAttribute("disabled");
  }
  set disabled(value) {
    this.toggleAttribute("disabled", Boolean(value));
  }
  connectedCallback() {
    if (isViewDefinition(this)) return;
    upgradeProperties(this, ["index", "active", "angle", "distance", "size", "disabled"]);
    if (!this._button) {
      this._button = this.querySelector(":scope > .z-pagination-button") ?? this.ownerDocument.createElement("button");
      this._button.type = "button";
      this._button.className = "z-pagination-button";
      this.append(this._button);
    }
    this.classList.add("z-pagination", "satellite");
    if (!this._listeners) {
      this._listeners = new this.ownerDocument.defaultView.AbortController();
      this._button.addEventListener("click", (event) => {
        event.stopPropagation();
        if (!this.disabled) emit(this, "change", { index: this.index, page: this.index + 1 });
      }, { signal: this._listeners.signal });
    }
    this._render();
  }
  disconnectedCallback() {
    this._listeners?.abort();
    this._listeners = null;
  }
  attributeChangedCallback() {
    this._render();
  }
  _render() {
    if (!this._button) return;
    this._button.textContent = String(this.index + 1);
    this._button.setAttribute("aria-label", `Page ${this.index + 1}`);
    this._button.disabled = this.disabled;
    this._button.toggleAttribute("data-active", this.index === this.active);
    if (this.index === this.active) this._button.setAttribute("aria-current", "page");
    else this._button.removeAttribute("aria-current");
    this.classList.toggle("active", this.index === this.active);
    this.classList.toggle("deactive", this.index !== this.active);
    this.style.setProperty("--o-from", `${this.angle}deg`);
    this.style.setProperty("--o-offset", "0deg");
    this.style.setProperty("--o-angle", "0deg");
    this.style.setProperty("--z-pagination-distance", String(this.distance / 100));
    this.style.setProperty("--o-aligment", `calc(var(--o-radius) * ${1 - this.distance / 100})`);
    this.style.setProperty("--z-pagination-size", `var(--z-size-${this.size}, var(--z-size-xs))`);
  }
};

// src/components/index.js
function registerElements() {
  if (typeof customElements === "undefined") return;
  registerOrbit();
  const elements = { "z-canvas": ZCanvas, "z-view": ZView, "z-spot": ZSpot, "z-list": ZList, "z-dialog": ZDialog, "z-knob": ZKnob, "z-slider": ZSlider, "z-scroll": ZScroll, "z-pagination": ZPagination };
  for (const [name, ctor] of Object.entries(elements)) {
    if (!customElements.get(name)) customElements.define(name, ctor);
  }
}

// src/zircle.js
import { Zumly as Zumly2, ZumlyRouter as ZumlyRouter2 } from "zumly";
import { Orbit as Orbit2 } from "@zumer/orbit";
if (typeof window !== "undefined") registerElements();
export {
  MODES,
  Orbit2 as Orbit,
  THEMES,
  ZCanvas,
  ZDialog,
  ZKnob,
  ZList,
  ZPagination,
  ZScroll,
  ZSlider,
  ZSpot,
  ZView,
  Zumly2 as Zumly,
  ZumlyRouter2 as ZumlyRouter,
  createZircle,
  createZircle as default,
  registerElements
};
//# sourceMappingURL=zircle.js.map