import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const styles = css` :host { background: var(--rs-bg-surface, #fff); color: var(--rs-text-primary, #1e293b); border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); min-width: 200px; min-height: 150px; } .header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #22c55e; color: white; border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 600; cursor: move; } .header-title { display: flex; align-items: center; gap: 6px; } .header-actions { display: flex; gap: 4px; } .header-actions button { background: transparent; border: none; color: white; cursor: pointer; padding: 2px 6px; border-radius: 4px; font-size: 14px; } .header-actions button:hover { background: rgba(255, 255, 255, 0.2); } .content { width: 100%; height: calc(100% - 36px); position: relative; border-radius: 0 0 8px 8px; overflow: hidden; } .drop-zone { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 24px; gap: 12px; border: 2px dashed var(--rs-input-border, #e2e8f0); border-radius: 0 0 8px 8px; margin: 8px; text-align: center; color: #94a3b8; font-size: 13px; transition: border-color 0.2s, background 0.2s; } .drop-zone.dragover { border-color: #22c55e; background: rgba(34, 197, 94, 0.05); } .drop-zone-icon { font-size: 32px; } .url-input { width: 100%; max-width: 300px; padding: 10px 14px; border: 2px solid var(--rs-input-border, #e2e8f0); border-radius: 8px; font-size: 13px; outline: none; background: var(--rs-input-bg, #fff); color: var(--rs-input-text, inherit); } .url-input:focus { border-color: #22c55e; } .image-display { width: 100%; height: 100%; object-fit: contain; display: block; } `; declare global { interface HTMLElementTagNameMap { "folk-image": FolkImage; } } export class FolkImage extends FolkShape { static override tagName = "folk-image"; 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; } #src: string | null = null; #alt: string = ""; get src() { return this.#src; } set src(value: string | null) { this.#src = value; this.requestUpdate("src"); this.dispatchEvent(new CustomEvent("src-change", { detail: { src: value } })); } get alt() { return this.#alt; } set alt(value: string) { this.#alt = value; } override createRenderRoot() { const root = super.createRenderRoot(); this.#src = this.getAttribute("src") || null; this.#alt = this.getAttribute("alt") || ""; const wrapper = document.createElement("div"); wrapper.innerHTML = html`
🖼️ Image
🖼️ Paste, drop, or enter image URL…
`; const slot = root.querySelector("slot"); const containerDiv = slot?.parentElement as HTMLElement; if (containerDiv) { containerDiv.replaceWith(wrapper); } const content = wrapper.querySelector(".content") as HTMLElement; const dropZone = wrapper.querySelector(".drop-zone") as HTMLElement; const urlInput = wrapper.querySelector(".url-input") as HTMLInputElement; const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; // URL input const handleUrlSubmit = () => { let inputUrl = urlInput.value.trim(); if (!inputUrl) return; if (!inputUrl.startsWith("http://") && !inputUrl.startsWith("https://")) { inputUrl = `https://${inputUrl}`; } this.src = inputUrl; this.#renderImage(content, dropZone); }; urlInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); handleUrlSubmit(); } }); urlInput.addEventListener("blur", () => { if (urlInput.value.trim()) handleUrlSubmit(); }); // Drop image files directly onto shape dropZone.addEventListener("dragover", (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add("dragover"); }); dropZone.addEventListener("dragleave", () => { dropZone.classList.remove("dragover"); }); dropZone.addEventListener("drop", (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove("dragover"); const file = Array.from(e.dataTransfer?.files || []).find((f) => f.type.startsWith("image/") ); if (file) { this.#uploadFile(file, content, dropZone); return; } const url = e.dataTransfer?.getData("text/plain") || ""; if (url.trim()) { this.src = url.trim(); this.#renderImage(content, dropZone); } }); // Close button closeBtn.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); // If src already set, render image if (this.#src) { this.#renderImage(content, dropZone); } return root; } async #uploadFile( file: File, content: HTMLElement, dropZone: HTMLElement ) { const reader = new FileReader(); reader.onload = async () => { try { const res = await fetch("/api/image-upload", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image: reader.result }), }); const data = await res.json(); if (data.url) { this.src = data.url; this.alt = file.name; this.#renderImage(content, dropZone); } } catch (err) { console.error("[folk-image] upload failed:", err); } }; reader.readAsDataURL(file); } #renderImage(content: HTMLElement, dropZone: HTMLElement) { if (!this.#src) return; dropZone.style.display = "none"; // Remove existing image if any const existing = content.querySelector(".image-display"); if (existing) existing.remove(); const img = document.createElement("img"); img.className = "image-display"; img.src = this.#src; img.alt = this.#alt || "Image"; img.draggable = false; content.appendChild(img); // Update header title const titleText = this.renderRoot.querySelector(".title-text"); if (titleText) { titleText.textContent = this.#alt || "Image"; } } static override fromData(data: Record): FolkImage { const shape = FolkShape.fromData(data) as FolkImage; if (data.src) shape.src = data.src; if (data.alt) shape.alt = data.alt; return shape; } override toJSON() { return { ...super.toJSON(), type: "folk-image", src: this.src, alt: this.alt, }; } override applyData(data: Record): void { super.applyData(data); if ("src" in data && this.src !== data.src) { this.src = data.src; } if ("alt" in data && this.alt !== data.alt) { this.alt = data.alt; } } }