import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const styles = css` :host { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); min-width: 300px; min-height: 200px; } .header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #eab308; 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; } .favicon { width: 16px; height: 16px; border-radius: 2px; } .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; } .url-input-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 24px; gap: 12px; } .url-input { width: 100%; max-width: 400px; padding: 12px 16px; border: 2px solid #e2e8f0; border-radius: 8px; font-size: 14px; outline: none; } .url-input:focus { border-color: #eab308; } .url-error { color: #ef4444; font-size: 12px; } .embed-iframe { width: 100%; height: 100%; border: none; border-radius: 0 0 8px 8px; } .unsupported { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 24px; gap: 12px; text-align: center; color: #64748b; } .open-link { background: #eab308; color: white; border: none; border-radius: 6px; padding: 8px 16px; cursor: pointer; font-size: 14px; } .open-link:hover { background: #ca8a04; } `; // URL transformation patterns function transformUrl(url: string): string | null { try { const parsed = new URL(url); const hostname = parsed.hostname.replace("www.", ""); // YouTube const youtubeMatch = url.match( /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/ ); if (youtubeMatch) { return `https://www.youtube.com/embed/${youtubeMatch[1]}`; } // Twitter/X const twitterMatch = url.match( /(?:twitter\.com|x\.com)\/([^\/\s?]+)(?:\/(?:status|tweets)\/(\d+)|$)/ ); if (twitterMatch) { if (twitterMatch[2]) { // Tweet embed return `https://platform.x.com/embed/Tweet.html?id=${twitterMatch[2]}`; } // Profile - not embeddable return null; } // Google Maps if (hostname.includes("google") && parsed.pathname.includes("/maps")) { // Already an embed URL if (parsed.pathname.includes("/embed")) return url; // Convert place/directions URLs would need API key return url; } // Gather.town if (hostname === "app.gather.town") { return url.replace("app.gather.town", "gather.town/embed"); } // Medium - not embeddable if (hostname.includes("medium.com")) { return null; } // Pass through other URLs return url; } catch { return null; } } function getFaviconUrl(url: string): string { try { const hostname = new URL(url).hostname; return `https://www.google.com/s2/favicons?domain=${hostname}&sz=32`; } catch { return ""; } } function getDisplayTitle(url: string): string { try { const hostname = new URL(url).hostname.replace("www.", ""); if (hostname.includes("youtube")) return "YouTube"; if (hostname.includes("twitter") || hostname.includes("x.com")) return "Twitter/X"; if (hostname.includes("google") && url.includes("/maps")) return "Google Maps"; return hostname; } catch { return "Embed"; } } declare global { interface HTMLElementTagNameMap { "folk-embed": FolkEmbed; } } export class FolkEmbed extends FolkShape { static override tagName = "folk-embed"; 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; } #url: string | null = null; #error: string | null = null; get url() { return this.#url; } set url(value: string | null) { this.#url = value; this.requestUpdate("url"); this.dispatchEvent(new CustomEvent("url-change", { detail: { url: value } })); } override createRenderRoot() { const root = super.createRenderRoot(); this.#url = this.getAttribute("url") || null; const wrapper = document.createElement("div"); wrapper.innerHTML = html`
This content cannot be embedded in an iframe.