zircle
Version:
Zircle's circular components, rebuilt with Orbit and Zumly for the web. No Vue required.
1,738 lines • 69.3 kB
JavaScript
/*! 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