/** * — Visual overlay for shape groups. * Renders a dashed border + header bar positioned over the bounding box * of grouped shapes. Not a FolkShape — it's a lightweight overlay element. */ const PADDING = 16; const HEADER_HEIGHT = 28; const template = document.createElement("template"); template.innerHTML = `
`; export class FolkGroupFrame extends HTMLElement { #groupId = ""; #name = ""; #icon = ""; #color = "#14b8a6"; #memberCount = 0; #collapsed = false; static get observedAttributes() { return ["group-id", "group-name", "group-icon", "group-color", "member-count", "collapsed"]; } constructor() { super(); this.attachShadow({ mode: "open" }); this.shadowRoot!.appendChild(template.content.cloneNode(true)); this.shadowRoot!.querySelector(".collapse-btn")!.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("group-toggle-collapse", { detail: { groupId: this.#groupId }, bubbles: true, composed: true, })); }); this.shadowRoot!.querySelector(".dissolve-btn")!.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("group-dissolve", { detail: { groupId: this.#groupId }, bubbles: true, composed: true, })); }); this.shadowRoot!.querySelector(".collapsed-summary")!.addEventListener("click", () => { this.dispatchEvent(new CustomEvent("group-toggle-collapse", { detail: { groupId: this.#groupId }, bubbles: true, composed: true, })); }); } attributeChangedCallback(name: string, _old: string | null, val: string | null) { switch (name) { case "group-id": this.#groupId = val || ""; break; case "group-name": this.#name = val || ""; break; case "group-icon": this.#icon = val || ""; break; case "group-color": this.#color = val || "#14b8a6"; this.style.setProperty("--group-color", this.#color); break; case "member-count": this.#memberCount = Number(val) || 0; break; case "collapsed": this.#collapsed = val !== null; break; } this.#render(); } /** Position the frame over a bounding box (canvas coords). */ setBounds(x: number, y: number, width: number, height: number) { this.style.left = `${x - PADDING}px`; this.style.top = `${y - PADDING - HEADER_HEIGHT}px`; this.style.width = `${width + PADDING * 2}px`; this.style.height = `${height + PADDING * 2 + HEADER_HEIGHT}px`; } #render() { const icon = this.shadowRoot!.querySelector(".icon") as HTMLElement; const name = this.shadowRoot!.querySelector(".name") as HTMLElement; const count = this.shadowRoot!.querySelector(".count") as HTMLElement; const collapseBtn = this.shadowRoot!.querySelector(".collapse-btn") as HTMLElement; const summary = this.shadowRoot!.querySelector(".collapsed-summary") as HTMLElement; icon.textContent = this.#icon; name.textContent = this.#name; count.textContent = `(${this.#memberCount})`; collapseBtn.textContent = this.#collapsed ? "+" : "−"; summary.textContent = `${this.#icon} ${this.#name} — ${this.#memberCount} shapes`; } get groupId() { return this.#groupId; } } if (!customElements.get("folk-group-frame")) { customElements.define("folk-group-frame", FolkGroupFrame); }