diff --git a/modules/rsocials/components/folk-socials-canvas.ts b/modules/rsocials/components/folk-socials-canvas.ts new file mode 100644 index 0000000..4b89f3c --- /dev/null +++ b/modules/rsocials/components/folk-socials-canvas.ts @@ -0,0 +1,590 @@ +/** + * — campaign & thread canvas view. + * + * Renders campaigns and threads as draggable card nodes on an SVG canvas + * with pan/zoom/drag interactions. Includes a Postiz slide-out panel for + * post scheduling. + */ + +interface CampaignPost { + id: string; + platform: string; + postType: string; + stepNumber: number; + content: string; + scheduledAt: string; + status: string; + hashtags: string[]; + phase: number; + phaseLabel: string; +} + +interface Campaign { + id: string; + title: string; + description: string; + duration: string; + platforms: string[]; + phases: { name: string; label: string; days: string }[]; + posts: CampaignPost[]; +} + +interface ThreadData { + id: string; + name: string; + handle: string; + title: string; + tweets: string[]; + imageUrl?: string; + createdAt: number; + updatedAt: number; +} + +interface CanvasNode { + id: string; + type: "campaign" | "thread"; + data: Campaign | ThreadData; + expanded: boolean; +} + +const PLATFORM_ICONS: Record = { + x: "\ud835\udd4f", linkedin: "in", instagram: "\ud83d\udcf7", + youtube: "\u25b6\ufe0f", threads: "\ud83e\uddf5", bluesky: "\ud83e\udd8b", +}; + +const PLATFORM_COLORS: Record = { + x: "#000000", linkedin: "#0A66C2", instagram: "#E4405F", + youtube: "#FF0000", threads: "#000000", bluesky: "#0085FF", +}; + +// Demo campaign inline (same as server campaign-data.ts) +const DEMO_CAMPAIGN: Campaign = { + id: "mycofi-earth-launch", + title: "MycoFi Earth Launch Campaign", + description: "Multi-platform product launch campaign for MycoFi Earth.", + duration: "Feb 21\u201325, 2026 (5 days)", + platforms: ["X", "LinkedIn", "Instagram", "YouTube", "Threads", "Bluesky"], + phases: [ + { name: "pre-launch", label: "Pre-Launch Hype", days: "Day -3 to -1" }, + { name: "launch", label: "Launch Day", days: "Day 0" }, + { name: "amplification", label: "Amplification", days: "Day +1" }, + ], + posts: [ + { id: "post-x-teaser", platform: "x", postType: "thread", stepNumber: 1, content: "Something is growing in the mycelium... \ud83c\udf44", scheduledAt: "2026-02-21T09:00:00", status: "scheduled", hashtags: ["MycoFi"], phase: 1, phaseLabel: "Pre-Launch Hype" }, + { id: "post-linkedin-thought", platform: "linkedin", postType: "article", stepNumber: 2, content: "The regenerative finance movement isn't just about returns...", scheduledAt: "2026-02-22T11:00:00", status: "scheduled", hashtags: ["RegenerativeFinance"], phase: 1, phaseLabel: "Pre-Launch Hype" }, + { id: "post-ig-carousel", platform: "instagram", postType: "carousel", stepNumber: 3, content: "5 Ways Mycelium Networks Mirror the Future of Finance", scheduledAt: "2026-02-23T14:00:00", status: "scheduled", hashtags: ["MycoFi"], phase: 1, phaseLabel: "Pre-Launch Hype" }, + { id: "post-yt-launch", platform: "youtube", postType: "video", stepNumber: 4, content: "MycoFi Earth \u2014 Official Launch Video", scheduledAt: "2026-02-24T10:00:00", status: "draft", hashtags: ["MycoFiLaunch"], phase: 2, phaseLabel: "Launch Day" }, + { id: "post-x-launch", platform: "x", postType: "thread", stepNumber: 5, content: "\ud83c\udf44 MycoFi Earth is LIVE \ud83c\udf44", scheduledAt: "2026-02-24T10:15:00", status: "draft", hashtags: ["MycoFi", "Launch"], phase: 2, phaseLabel: "Launch Day" }, + { id: "post-threads-xpost", platform: "threads", postType: "text", stepNumber: 8, content: "We just launched MycoFi Earth and the response has been incredible", scheduledAt: "2026-02-25T14:00:00", status: "draft", hashtags: ["MycoFi"], phase: 3, phaseLabel: "Amplification" }, + ], +}; + +const DEMO_THREADS: ThreadData[] = [ + { id: "t-demo-1", name: "Jeff Emmett", handle: "@jeffemmett", title: "Why Regenerative Finance Needs Mycelial Networks", tweets: ["The old financial system is composting itself. Here's why \ud83e\uddf5\ud83d\udc47", "Mycelium doesn't hoard \u2014 it redistributes nutrients.", "What if finance worked the same way?"], createdAt: Date.now() - 86400000, updatedAt: Date.now() - 3600000 }, + { id: "t-demo-2", name: "Commons DAO", handle: "@commonsdao", title: "Governance Patterns for Web3 Communities", tweets: ["Thread: 7 governance patterns we've tested at Commons DAO", "1/ Conviction voting \u2014 signal strength over time", "2/ Composting \u2014 failed proposals return resources to the pool"], createdAt: Date.now() - 172800000, updatedAt: Date.now() - 86400000 }, +]; + +class FolkSocialsCanvas extends HTMLElement { + private shadow: ShadowRoot; + private space = ""; + + // Data + private campaigns: Campaign[] = []; + private threads: ThreadData[] = []; + private canvasNodes: CanvasNode[] = []; + + // Canvas state + private canvasZoom = 1; + private canvasPanX = 0; + private canvasPanY = 0; + private nodePositions: Record = {}; + private draggingNodeId: string | null = null; + private dragStartX = 0; + private dragStartY = 0; + private dragNodeStartX = 0; + private dragNodeStartY = 0; + private isPanning = false; + private panStartX = 0; + private panStartY = 0; + private panStartPanX = 0; + private panStartPanY = 0; + private isTouchPanning = false; + private lastTouchCenter: { x: number; y: number } | null = null; + private lastTouchDist: number | null = null; + + // Postiz panel + private postizOpen = false; + + constructor() { + super(); + this.shadow = this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.space = this.getAttribute("space") || "demo"; + this.loadData(); + } + + private async loadData() { + if (this.space === "demo") { + this.campaigns = [DEMO_CAMPAIGN]; + this.threads = DEMO_THREADS; + } else { + try { + const base = this.getApiBase(); + const [campRes, threadRes] = await Promise.all([ + fetch(`${base}/api/campaigns`).catch(() => null), + fetch(`${base}/api/threads`).catch(() => null), + ]); + if (campRes?.ok) { + const data = await campRes.json(); + this.campaigns = Array.isArray(data) ? data : (data.campaigns || [DEMO_CAMPAIGN]); + } else { + this.campaigns = [DEMO_CAMPAIGN]; + } + if (threadRes?.ok) { + this.threads = await threadRes.json(); + } + } catch { + this.campaigns = [DEMO_CAMPAIGN]; + this.threads = DEMO_THREADS; + } + } + + this.buildCanvasNodes(); + this.layoutNodes(); + this.render(); + requestAnimationFrame(() => this.fitView()); + } + + private getApiBase(): string { + const path = window.location.pathname; + const match = path.match(/^(\/[^/]+)?\/rsocials/); + return match ? match[0] : ""; + } + + private buildCanvasNodes() { + this.canvasNodes = []; + for (const c of this.campaigns) { + this.canvasNodes.push({ id: `camp-${c.id}`, type: "campaign", data: c, expanded: false }); + } + for (const t of this.threads) { + this.canvasNodes.push({ id: `thread-${t.id}`, type: "thread", data: t, expanded: false }); + } + } + + private layoutNodes() { + // Grid layout: campaigns top row, threads below + const campGap = 380; + const threadGap = 340; + let cx = 50; + for (const node of this.canvasNodes) { + if (node.type === "campaign") { + this.nodePositions[node.id] = { x: cx, y: 50 }; + cx += campGap; + } + } + let tx = 50; + for (const node of this.canvasNodes) { + if (node.type === "thread") { + this.nodePositions[node.id] = { x: tx, y: 350 }; + tx += threadGap; + } + } + } + + private getNodeSize(node: CanvasNode): { w: number; h: number } { + if (node.type === "campaign") { + const camp = node.data as Campaign; + if (node.expanded) { + return { w: 320, h: 200 + camp.posts.length * 48 }; + } + return { w: 320, h: 200 }; + } else { + const thread = node.data as ThreadData; + if (node.expanded) { + return { w: 280, h: 140 + thread.tweets.length * 44 }; + } + return { w: 280, h: 140 }; + } + } + + // ── Canvas transform ── + + private updateCanvasTransform() { + const g = this.shadow.getElementById("canvas-transform"); + if (g) g.setAttribute("transform", `translate(${this.canvasPanX},${this.canvasPanY}) scale(${this.canvasZoom})`); + } + + private fitView() { + const svg = this.shadow.getElementById("socials-svg") as SVGSVGElement | null; + if (!svg) return; + if (this.canvasNodes.length === 0) return; + + const pad = 60; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const node of this.canvasNodes) { + const pos = this.nodePositions[node.id]; + if (!pos) continue; + const size = this.getNodeSize(node); + if (pos.x < minX) minX = pos.x; + if (pos.y < minY) minY = pos.y; + if (pos.x + size.w > maxX) maxX = pos.x + size.w; + if (pos.y + size.h > maxY) maxY = pos.y + size.h; + } + + const contentW = maxX - minX; + const contentH = maxY - minY; + const rect = svg.getBoundingClientRect(); + const svgW = rect.width || 800; + const svgH = rect.height || 600; + + const zoom = Math.min((svgW - pad * 2) / Math.max(contentW, 1), (svgH - pad * 2) / Math.max(contentH, 1), 2); + this.canvasZoom = Math.max(0.1, Math.min(zoom, 4)); + this.canvasPanX = (svgW / 2) - ((minX + maxX) / 2) * this.canvasZoom; + this.canvasPanY = (svgH / 2) - ((minY + maxY) / 2) * this.canvasZoom; + this.updateCanvasTransform(); + this.updateZoomDisplay(); + } + + private updateZoomDisplay() { + const el = this.shadow.getElementById("zoom-level"); + if (el) el.textContent = `${Math.round(this.canvasZoom * 100)}%`; + } + + private zoomAt(screenX: number, screenY: number, factor: number) { + const oldZoom = this.canvasZoom; + const newZoom = Math.max(0.1, Math.min(4, oldZoom * factor)); + this.canvasPanX = screenX - (screenX - this.canvasPanX) * (newZoom / oldZoom); + this.canvasPanY = screenY - (screenY - this.canvasPanY) * (newZoom / oldZoom); + this.canvasZoom = newZoom; + this.updateCanvasTransform(); + this.updateZoomDisplay(); + } + + // ── Render ── + + private renderCampaignNode(node: CanvasNode): string { + const camp = node.data as Campaign; + const pos = this.nodePositions[node.id]; + if (!pos) return ""; + const { w, h } = this.getNodeSize(node); + + const scheduled = camp.posts.filter(p => p.status === "scheduled").length; + const draft = camp.posts.filter(p => p.status === "draft").length; + const phaseProgress = Math.round((scheduled / Math.max(camp.posts.length, 1)) * 100); + + const platformIcons = camp.platforms.map(p => { + const key = p.toLowerCase(); + const icon = PLATFORM_ICONS[key] || p.charAt(0); + const color = PLATFORM_COLORS[key] || "#888"; + return `${icon}`; + }).join(""); + + let postsHtml = ""; + if (node.expanded) { + postsHtml = camp.posts.map(p => { + const icon = PLATFORM_ICONS[p.platform] || p.platform; + const statusColor = p.status === "scheduled" ? "#22c55e" : "#f59e0b"; + const preview = p.content.substring(0, 60) + (p.content.length > 60 ? "\u2026" : ""); + return ` +
+ ${icon} + ${this.esc(preview)} + +
`; + }).join(""); + } + + return ` + + +
+
+
+ \ud83d\udce2 + ${this.esc(camp.title)} + ${node.expanded ? "\u25b2" : "\u25bc"} +
+
${this.esc(camp.duration)}
+
${platformIcons}
+
+
+
+
+ ${scheduled} scheduled + ${draft} draft +
+
+ ${postsHtml} +
+
+
`; + } + + private renderThreadNode(node: CanvasNode): string { + const thread = node.data as ThreadData; + const pos = this.nodePositions[node.id]; + if (!pos) return ""; + const { w, h } = this.getNodeSize(node); + + const initial = (thread.name || "?").charAt(0).toUpperCase(); + const dateStr = new Date(thread.updatedAt).toLocaleDateString("en-US", { month: "short", day: "numeric" }); + + let tweetsHtml = ""; + if (node.expanded) { + tweetsHtml = thread.tweets.map((t, i) => { + const preview = t.substring(0, 80) + (t.length > 80 ? "\u2026" : ""); + return ` +
+ ${i + 1}. + ${this.esc(preview)} +
`; + }).join(""); + } + + return ` + + +
+
+
+ \ud83e\uddf5 + ${this.esc(thread.title)} + ${node.expanded ? "\u25b2" : "\u25bc"} +
+
+ ${initial} + ${this.esc(thread.handle || thread.name)} + ${thread.tweets.length} tweet${thread.tweets.length === 1 ? "" : "s"} + \u00b7 + ${dateStr} +
+
+ ${tweetsHtml} +
+
+
`; + } + + private render() { + const nodesSvg = this.canvasNodes.map(node => { + if (node.type === "campaign") return this.renderCampaignNode(node); + return this.renderThreadNode(node); + }).join(""); + + this.shadow.innerHTML = ` + +
+
+ \ud83d\udce2 rSocials Canvas${this.space === "demo" ? 'Demo' : ""} +
+ +
+
+ +
+
+ + + ${nodesSvg} + + +
+ + ${Math.round(this.canvasZoom * 100)}% + + +
+
+ +
+
+ Postiz — Post Scheduler + +
+ +
+
+
+ `; + + this.attachListeners(); + } + + private attachListeners() { + // Postiz panel toggle + this.shadow.getElementById("toggle-postiz")?.addEventListener("click", () => { + this.postizOpen = !this.postizOpen; + const panel = this.shadow.getElementById("postiz-panel"); + const iframe = panel?.querySelector("iframe"); + if (panel) panel.classList.toggle("open", this.postizOpen); + if (iframe && this.postizOpen) iframe.src = "https://social.jeffemmett.com"; + if (iframe && !this.postizOpen) iframe.src = "about:blank"; + }); + this.shadow.getElementById("close-postiz")?.addEventListener("click", () => { + this.postizOpen = false; + const panel = this.shadow.getElementById("postiz-panel"); + const iframe = panel?.querySelector("iframe"); + if (panel) panel.classList.remove("open"); + if (iframe) iframe.src = "about:blank"; + }); + + // Zoom controls + this.shadow.getElementById("zoom-in")?.addEventListener("click", () => { + const svg = this.shadow.getElementById("socials-svg") as SVGSVGElement | null; + if (!svg) return; + const rect = svg.getBoundingClientRect(); + this.zoomAt(rect.width / 2, rect.height / 2, 1.25); + }); + this.shadow.getElementById("zoom-out")?.addEventListener("click", () => { + const svg = this.shadow.getElementById("socials-svg") as SVGSVGElement | null; + if (!svg) return; + const rect = svg.getBoundingClientRect(); + this.zoomAt(rect.width / 2, rect.height / 2, 0.8); + }); + this.shadow.getElementById("zoom-fit")?.addEventListener("click", () => this.fitView()); + + // Canvas interactions + const canvas = this.shadow.getElementById("sc-canvas"); + const svg = this.shadow.getElementById("socials-svg") as SVGSVGElement | null; + if (!svg || !canvas) return; + + // Node click → expand/collapse + this.shadow.querySelectorAll("[data-canvas-node]").forEach(el => { + const nodeEl = el as SVGGElement; + // We handle click vs drag in pointerup + }); + + // Wheel zoom + svg.addEventListener("wheel", (e: WheelEvent) => { + e.preventDefault(); + const rect = svg.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + const factor = 1 - e.deltaY * 0.003; + this.zoomAt(mx, my, factor); + }, { passive: false }); + + // Pointer down — node drag or canvas pan + svg.addEventListener("pointerdown", (e: PointerEvent) => { + if (e.button !== 0) return; + const target = (e.target as Element).closest("[data-canvas-node]") as HTMLElement | null; + + if (target) { + const nodeId = target.dataset.canvasNode!; + this.draggingNodeId = nodeId; + this.dragStartX = e.clientX; + this.dragStartY = e.clientY; + const pos = this.nodePositions[nodeId]; + if (pos) { + this.dragNodeStartX = pos.x; + this.dragNodeStartY = pos.y; + } + svg.setPointerCapture(e.pointerId); + e.preventDefault(); + } else { + this.isPanning = true; + this.panStartX = e.clientX; + this.panStartY = e.clientY; + this.panStartPanX = this.canvasPanX; + this.panStartPanY = this.canvasPanY; + canvas.classList.add("grabbing"); + svg.setPointerCapture(e.pointerId); + e.preventDefault(); + } + }); + + svg.addEventListener("pointermove", (e: PointerEvent) => { + if (this.draggingNodeId) { + const dx = (e.clientX - this.dragStartX) / this.canvasZoom; + const dy = (e.clientY - this.dragStartY) / this.canvasZoom; + const pos = this.nodePositions[this.draggingNodeId]; + if (pos) { + pos.x = this.dragNodeStartX + dx; + pos.y = this.dragNodeStartY + dy; + // Move the foreignObject directly + const g = this.shadow.querySelector(`[data-canvas-node="${this.draggingNodeId}"]`); + const fo = g?.querySelector("foreignObject"); + if (fo) { + fo.setAttribute("x", String(pos.x)); + fo.setAttribute("y", String(pos.y)); + } + } + } else if (this.isPanning) { + this.canvasPanX = this.panStartPanX + (e.clientX - this.panStartX); + this.canvasPanY = this.panStartPanY + (e.clientY - this.panStartY); + this.updateCanvasTransform(); + } + }); + + svg.addEventListener("pointerup", (e: PointerEvent) => { + if (this.draggingNodeId) { + const dx = Math.abs(e.clientX - this.dragStartX); + const dy = Math.abs(e.clientY - this.dragStartY); + if (dx < 4 && dy < 4) { + // Click — toggle expand + const node = this.canvasNodes.find(n => n.id === this.draggingNodeId); + if (node) { + node.expanded = !node.expanded; + this.draggingNodeId = null; + this.render(); + requestAnimationFrame(() => this.updateCanvasTransform()); + return; + } + } + this.draggingNodeId = null; + } + if (this.isPanning) { + this.isPanning = false; + canvas.classList.remove("grabbing"); + } + }); + + // Touch pinch-zoom + svg.addEventListener("touchstart", (e: TouchEvent) => { + if (e.touches.length === 2) { + e.preventDefault(); + this.isTouchPanning = true; + const t0 = e.touches[0], t1 = e.touches[1]; + this.lastTouchCenter = { x: (t0.clientX + t1.clientX) / 2, y: (t0.clientY + t1.clientY) / 2 }; + this.lastTouchDist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY); + } + }, { passive: false }); + + svg.addEventListener("touchmove", (e: TouchEvent) => { + if (e.touches.length === 2 && this.isTouchPanning) { + e.preventDefault(); + const t0 = e.touches[0], t1 = e.touches[1]; + const center = { x: (t0.clientX + t1.clientX) / 2, y: (t0.clientY + t1.clientY) / 2 }; + const dist = Math.hypot(t1.clientX - t0.clientX, t1.clientY - t0.clientY); + + if (this.lastTouchCenter && this.lastTouchDist) { + this.canvasPanX += center.x - this.lastTouchCenter.x; + this.canvasPanY += center.y - this.lastTouchCenter.y; + const rect = svg.getBoundingClientRect(); + const mx = center.x - rect.left; + const my = center.y - rect.top; + const factor = dist / this.lastTouchDist; + this.zoomAt(mx, my, factor); + } + this.lastTouchCenter = center; + this.lastTouchDist = dist; + } + }, { passive: false }); + + svg.addEventListener("touchend", () => { + this.isTouchPanning = false; + this.lastTouchCenter = null; + this.lastTouchDist = null; + }); + } + + private esc(s: string): string { + const d = document.createElement("div"); + d.textContent = s || ""; + return d.innerHTML; + } +} + +customElements.define("folk-socials-canvas", FolkSocialsCanvas); diff --git a/modules/rsocials/components/socials-canvas.css b/modules/rsocials/components/socials-canvas.css new file mode 100644 index 0000000..f2500eb --- /dev/null +++ b/modules/rsocials/components/socials-canvas.css @@ -0,0 +1,215 @@ +/* rSocials Canvas — dark theme */ +folk-socials-canvas { + display: block; + height: calc(100vh - 60px); +} + +.socials-canvas-root { + display: flex; + flex-direction: column; + height: 100%; + font-family: system-ui, -apple-system, sans-serif; + color: var(--rs-text-primary); +} + +/* Toolbar */ +.sc-toolbar { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 20px; + min-height: 48px; + border-bottom: 1px solid var(--rs-border); +} + +.sc-toolbar__title { + font-size: 15px; + font-weight: 600; + flex: 1; +} + +.sc-demo-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + background: #f59e0b22; + color: #f59e0b; + font-size: 11px; + font-weight: 600; + margin-left: 8px; +} + +.sc-toolbar__actions { + display: flex; + gap: 8px; +} + +.sc-btn { + padding: 6px 14px; + border-radius: 8px; + border: 1px solid var(--rs-input-border); + background: var(--rs-input-bg); + color: var(--rs-text-primary); + font-size: 12px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} + +.sc-btn:hover { + border-color: var(--rs-border-strong); +} + +.sc-btn--postiz { + background: #6366f122; + border-color: #6366f155; + color: #818cf8; +} + +.sc-btn--postiz:hover { + background: #6366f133; + border-color: #6366f1; +} + +/* Canvas area — fills remaining space */ +.sc-canvas-area { + flex: 1; + display: flex; + overflow: hidden; + position: relative; +} + +.sc-canvas { + flex: 1; + position: relative; + overflow: hidden; + cursor: grab; + background: var(--rs-canvas-bg, #0f0f23); +} + +.sc-canvas.grabbing { + cursor: grabbing; +} + +.sc-canvas svg { + display: block; + width: 100%; + height: 100%; +} + +/* Zoom controls */ +.sc-zoom-controls { + position: absolute; + bottom: 12px; + right: 12px; + display: flex; + align-items: center; + gap: 4px; + background: var(--rs-bg-surface); + border: 1px solid var(--rs-border-strong); + border-radius: 8px; + padding: 4px 6px; + z-index: 5; +} + +.sc-zoom-btn { + width: 28px; + height: 28px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--rs-text-primary); + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s; +} + +.sc-zoom-btn:hover { + background: var(--rs-bg-surface-raised); +} + +.sc-zoom-btn--fit { + font-size: 14px; +} + +.sc-zoom-level { + font-size: 11px; + color: var(--rs-text-muted); + min-width: 36px; + text-align: center; +} + +/* Postiz slide-out panel */ +.sc-postiz-panel { + width: 0; + overflow: hidden; + border-left: 1px solid var(--rs-border); + background: var(--rs-bg-surface); + display: flex; + flex-direction: column; + transition: width 0.3s ease; +} + +.sc-postiz-panel.open { + width: 60%; + min-width: 400px; +} + +.sc-postiz-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-bottom: 1px solid var(--rs-border); + min-height: 44px; +} + +.sc-postiz-title { + font-size: 13px; + font-weight: 600; + flex: 1; +} + +.sc-postiz-close { + background: none; + border: none; + color: var(--rs-text-muted); + font-size: 16px; + cursor: pointer; + padding: 4px; +} + +.sc-postiz-close:hover { + color: var(--rs-text-primary); +} + +.sc-postiz-iframe { + flex: 1; + border: none; + width: 100%; +} + +/* Canvas node hover */ +.canvas-node foreignObject div:hover { + border-color: #4f46e5 !important; +} + +/* Mobile */ +@media (max-width: 768px) { + .sc-postiz-panel.open { + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + z-index: 20; + min-width: unset; + } + + .sc-toolbar { + flex-wrap: wrap; + padding: 8px 12px; + } +} diff --git a/vite.config.ts b/vite.config.ts index c1e82db..2c7b004 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -244,8 +244,15 @@ export default defineConfig({ await build({ configFile: false, root: resolve(__dirname, "modules/rflows/components"), - resolve: { alias: flowsAlias }, + plugins: [wasm()], + resolve: { + alias: { + ...flowsAlias, + '@automerge/automerge': resolve(__dirname, 'node_modules/@automerge/automerge'), + }, + }, build: { + target: "esnext", emptyOutDir: false, outDir: resolve(__dirname, "dist/modules/rflows"), lib: {