import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const styles = css` :host { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); min-width: 200px; min-height: 100px; display: flex; flex-direction: column; overflow: hidden; } :host([selected]) { box-shadow: 0 0 0 2px var(--primary-color, #14b8a6), 0 4px 8px rgba(0, 0, 0, 0.15); } .header { height: 40px; display: flex; align-items: center; justify-content: space-between; padding: 0 12px; cursor: move; user-select: none; flex-shrink: 0; transition: background-color 0.2s ease; background: var(--header-bg, rgba(20, 184, 166, 0.1)); border-bottom: 1px solid var(--header-border, rgba(20, 184, 166, 0.2)); } :host([selected]) .header { background: var(--primary-color, #14b8a6); color: white; } .header-title { font-size: 13px; font-weight: 600; color: var(--primary-color, #14b8a6); display: flex; align-items: center; gap: 6px; } :host([selected]) .header-title { color: white; } .header-icon { font-size: 14px; } .header-actions { display: flex; gap: 8px; align-items: center; } .header-actions button { width: 24px; height: 24px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; transition: background-color 0.15s ease; background: var(--button-bg, rgba(20, 184, 166, 0.2)); color: var(--primary-color, #14b8a6); } :host([selected]) .header-actions button { background: rgba(255, 255, 255, 0.2); color: white; } .header-actions button:hover { background: var(--button-hover-bg, rgba(20, 184, 166, 0.3)); } :host([selected]) .header-actions button:hover { background: rgba(255, 255, 255, 0.3); } .header-actions button.active { background: var(--primary-color, #14b8a6); color: white; } :host([selected]) .header-actions button.active { background: rgba(255, 255, 255, 0.4); } .content { flex: 1; overflow: auto; position: relative; } :host([minimized]) .content { display: none; } .tags { padding: 8px 12px; border-top: 1px solid #e0e0e0; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; min-height: 32px; background: #f8f9fa; flex-shrink: 0; } :host([minimized]) .tags { display: none; } .tag { background: #6b7280; color: white; padding: 4px 8px; border-radius: 12px; font-size: 10px; font-weight: 500; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; } .tag:hover { background: #4b5563; } .tag .remove { font-size: 8px; opacity: 0.7; } .tag .remove:hover { opacity: 1; } .add-tag { background: #9ca3af; color: white; border: none; border-radius: 12px; padding: 4px 10px; font-size: 10px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 4px; } .add-tag:hover { background: #6b7280; } .tag-input { border: 1px solid #9ca3af; border-radius: 12px; padding: 2px 6px; font-size: 10px; outline: none; min-width: 60px; flex: 1; background: white; } `; declare global { interface HTMLElementTagNameMap { "folk-wrapper": FolkWrapper; } } export class FolkWrapper extends FolkShape { static override tagName = "folk-wrapper"; // Merge parent and child styles static { const sheet = new CSSStyleSheet(); const parentRules = Array.from(FolkShape.styles.cssRules) .map((r) => r.cssText) .join("\n"); const childRules = Array.from(styles.cssRules) .map((r) => r.cssText) .join("\n"); sheet.replaceSync(`${parentRules}\n${childRules}`); this.styles = sheet; } #title = "Untitled"; #icon = ""; #primaryColor = "#14b8a6"; #isMinimized = false; #isPinned = false; #tags: string[] = []; #isEditingTags = false; #tagInput: HTMLInputElement | null = null; get title() { return this.#title; } set title(value: string) { this.#title = value; this.requestUpdate("title"); } get icon() { return this.#icon; } set icon(value: string) { this.#icon = value; this.requestUpdate("icon"); } get primaryColor() { return this.#primaryColor; } set primaryColor(value: string) { this.#primaryColor = value; this.style.setProperty("--primary-color", value); this.style.setProperty("--header-bg", `${value}10`); this.style.setProperty("--header-border", `${value}30`); this.style.setProperty("--button-bg", `${value}20`); this.style.setProperty("--button-hover-bg", `${value}30`); this.requestUpdate("primaryColor"); } get isMinimized() { return this.#isMinimized; } set isMinimized(value: boolean) { this.#isMinimized = value; this.toggleAttribute("minimized", value); } get isPinned() { return this.#isPinned; } set isPinned(value: boolean) { this.#isPinned = value; this.requestUpdate("isPinned"); } get tags() { return [...this.#tags]; } set tags(value: string[]) { this.#tags = [...value]; this.requestUpdate("tags"); } override createRenderRoot() { const root = super.createRenderRoot(); // Add wrapper UI const wrapper = document.createElement("div"); wrapper.innerHTML = html`
`; // Replace existing content structure const existingSlot = root.querySelector("slot"); if (existingSlot?.parentElement) { existingSlot.parentElement.innerHTML = ""; existingSlot.parentElement.appendChild(wrapper); } // Get references const header = wrapper.querySelector(".header") as HTMLElement; const titleIcon = wrapper.querySelector(".header-icon") as HTMLElement; const titleText = wrapper.querySelector(".title-text") as HTMLElement; const pinBtn = wrapper.querySelector(".pin-btn") as HTMLButtonElement; const minimizeBtn = wrapper.querySelector(".minimize-btn") as HTMLButtonElement; const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; const tagsContainer = wrapper.querySelector(".tags") as HTMLElement; // Initialize titleIcon.textContent = this.#icon; titleText.textContent = this.#title; this.primaryColor = this.getAttribute("color") || this.#primaryColor; this.title = this.getAttribute("title") || this.#title; this.icon = this.getAttribute("icon") || this.#icon; // Parse tags from attribute const tagsAttr = this.getAttribute("tags"); if (tagsAttr) { this.#tags = tagsAttr.split(",").map((t) => t.trim()).filter(Boolean); } // Event handlers pinBtn.addEventListener("click", (e) => { e.stopPropagation(); this.#isPinned = !this.#isPinned; pinBtn.classList.toggle("active", this.#isPinned); this.dispatchEvent(new CustomEvent("pin-toggle", { detail: { pinned: this.#isPinned } })); }); minimizeBtn.addEventListener("click", (e) => { e.stopPropagation(); this.isMinimized = !this.isMinimized; this.dispatchEvent(new CustomEvent("minimize-toggle", { detail: { minimized: this.#isMinimized } })); }); closeBtn.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); // Render tags this.#renderTags(tagsContainer); // Watch for attribute changes const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === "attributes") { switch (mutation.attributeName) { case "title": titleText.textContent = this.getAttribute("title") || ""; break; case "icon": titleIcon.textContent = this.getAttribute("icon") || ""; break; case "color": this.primaryColor = this.getAttribute("color") || "#14b8a6"; break; case "tags": const tagsAttr = this.getAttribute("tags"); if (tagsAttr) { this.#tags = tagsAttr.split(",").map((t) => t.trim()).filter(Boolean); this.#renderTags(tagsContainer); } break; } } } }); observer.observe(this, { attributes: true }); return root; } #renderTags(container: HTMLElement) { container.innerHTML = ""; for (const tag of this.#tags.slice(0, 5)) { const tagEl = document.createElement("span"); tagEl.className = "tag"; tagEl.innerHTML = `${tag.replace("#", "")} ×`; tagEl.querySelector(".remove")?.addEventListener("click", (e) => { e.stopPropagation(); this.#tags = this.#tags.filter((t) => t !== tag); this.#renderTags(container); this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } })); }); container.appendChild(tagEl); } if (this.#tags.length > 5) { const moreTag = document.createElement("span"); moreTag.className = "tag"; moreTag.textContent = `+${this.#tags.length - 5}`; container.appendChild(moreTag); } // Add tag button if (this.#tags.length < 10) { if (this.#isEditingTags) { const input = document.createElement("input"); input.className = "tag-input"; input.placeholder = "Add tag..."; input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); const value = input.value.trim().replace("#", ""); if (value && !this.#tags.includes(value)) { this.#tags.push(value); this.#renderTags(container); this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } })); } this.#isEditingTags = false; this.#renderTags(container); } else if (e.key === "Escape") { this.#isEditingTags = false; this.#renderTags(container); } }); input.addEventListener("blur", () => { const value = input.value.trim().replace("#", ""); if (value && !this.#tags.includes(value)) { this.#tags.push(value); this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } })); } this.#isEditingTags = false; this.#renderTags(container); }); container.appendChild(input); setTimeout(() => input.focus(), 0); } else { const addBtn = document.createElement("button"); addBtn.className = "add-tag"; addBtn.textContent = "+ Add"; addBtn.addEventListener("click", (e) => { e.stopPropagation(); this.#isEditingTags = true; this.#renderTags(container); }); container.appendChild(addBtn); } } } addTag(tag: string) { const cleanTag = tag.trim().replace("#", ""); if (cleanTag && !this.#tags.includes(cleanTag)) { this.#tags.push(cleanTag); const tagsContainer = this.shadowRoot?.querySelector(".tags") as HTMLElement; if (tagsContainer) { this.#renderTags(tagsContainer); } this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } })); } } removeTag(tag: string) { const cleanTag = tag.trim().replace("#", ""); this.#tags = this.#tags.filter((t) => t !== cleanTag); const tagsContainer = this.shadowRoot?.querySelector(".tags") as HTMLElement; if (tagsContainer) { this.#renderTags(tagsContainer); } this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } })); } toJSON() { return { type: "folk-wrapper", id: this.id, x: this.x, y: this.y, width: this.width, height: this.height, rotation: this.rotation, title: this.#title, icon: this.#icon, primaryColor: this.#primaryColor, isMinimized: this.#isMinimized, isPinned: this.#isPinned, tags: this.#tags, }; } }