fix: zoom-aware drag, minimum-penetration collision, replace unicode escapes
- Shape drag now accounts for canvas CSS transform scale so elements track the cursor correctly at all zoom levels - Collision resolution uses minimum penetration depth algorithm instead of movement-direction bias, preventing elements from flipping sides - Replaced all \uD83D surrogate pair escapes and \u00D7/\u276E/\u276F/ \u2022 escapes with actual Unicode characters across 60+ files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6fe02697a0
commit
9742bd1409
|
|
@ -411,7 +411,7 @@ export class FolkBlender extends FolkShape {
|
|||
if (this.#renderUrl) {
|
||||
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#renderUrl)}" alt="3D Render" />`;
|
||||
} else {
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u2705</span><span>Script generated (see Script tab)</span></div>';
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">✅</span><span>Script generated (see Script tab)</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -150,15 +150,15 @@ const styles = css`
|
|||
`;
|
||||
|
||||
const BOOKING_TYPE_ICONS: Record<string, string> = {
|
||||
FLIGHT: "\u2708\uFE0F",
|
||||
HOTEL: "\uD83C\uDFE8",
|
||||
CAR_RENTAL: "\uD83D\uDE97",
|
||||
TRAIN: "\uD83D\uDE84",
|
||||
BUS: "\uD83D\uDE8C",
|
||||
FERRY: "\u26F4\uFE0F",
|
||||
ACTIVITY: "\uD83C\uDFAF",
|
||||
RESTAURANT: "\uD83C\uDF7D\uFE0F",
|
||||
OTHER: "\uD83D\uDCCC",
|
||||
FLIGHT: "✈️",
|
||||
HOTEL: "🏨",
|
||||
CAR_RENTAL: "🚗",
|
||||
TRAIN: "🚄",
|
||||
BUS: "🚌",
|
||||
FERRY: "⛴️",
|
||||
ACTIVITY: "🎯",
|
||||
RESTAURANT: "🍽️",
|
||||
OTHER: "📌",
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
|
@ -253,11 +253,11 @@ export class FolkBooking extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header OTHER">
|
||||
<span class="header-title">
|
||||
<span class="type-icon">\uD83D\uDCCC</span>
|
||||
<span class="type-icon">📌</span>
|
||||
<span class="type-label">Booking</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="booking-body"></div>
|
||||
|
|
|
|||
|
|
@ -243,11 +243,11 @@ export class FolkBudget extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\uD83D\uDCB0</span>
|
||||
<span>💰</span>
|
||||
<span>Budget</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="budget-body">
|
||||
|
|
|
|||
|
|
@ -242,18 +242,18 @@ export class FolkCalendar extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\u{1F4C5}</span>
|
||||
<span>📅</span>
|
||||
<span>Calendar</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-nav">
|
||||
<button class="prev-btn">\u276E</button>
|
||||
<button class="prev-btn">❮</button>
|
||||
<span class="month-year"></span>
|
||||
<button class="next-btn">\u276F</button>
|
||||
<button class="next-btn">❯</button>
|
||||
</div>
|
||||
<div class="weekdays">
|
||||
<span class="weekday">Sun</span>
|
||||
|
|
|
|||
|
|
@ -277,15 +277,15 @@ export class FolkCanvas extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header" data-drag>
|
||||
<div class="header-left">
|
||||
<span class="icon">\u{1F5BC}</span>
|
||||
<span class="icon">🖼</span>
|
||||
<span class="source-name"></span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="permission-badge badge"></span>
|
||||
<div class="header-actions">
|
||||
<button class="collapse-btn" title="Toggle collapse">\u25BC</button>
|
||||
<button class="enter-space-btn" title="Open space">\u2197</button>
|
||||
<button class="close-btn" title="Remove">\u00D7</button>
|
||||
<button class="collapse-btn" title="Toggle collapse">▼</button>
|
||||
<button class="enter-space-btn" title="Open space">↗</button>
|
||||
<button class="close-btn" title="Remove">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -331,7 +331,7 @@ export class FolkCanvas extends FolkShape {
|
|||
collapseBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.collapsed = !this.#collapsed;
|
||||
collapseBtn.textContent = this.#collapsed ? "\u25B6" : "\u25BC";
|
||||
collapseBtn.textContent = this.#collapsed ? "▶" : "▼";
|
||||
this.dispatchEvent(new CustomEvent("content-change", { detail: { collapsed: this.#collapsed } }));
|
||||
});
|
||||
|
||||
|
|
@ -448,10 +448,10 @@ export class FolkCanvas extends FolkShape {
|
|||
if (this.#collapsed) {
|
||||
content.innerHTML = `
|
||||
<div class="collapsed-view">
|
||||
<div class="collapsed-icon">\u{1F5BC}</div>
|
||||
<div class="collapsed-icon">🖼</div>
|
||||
<div class="collapsed-label">${this.#label || this.#sourceSlug}</div>
|
||||
<div class="collapsed-meta">${this.#nestedShapes.size} shapes</div>
|
||||
<button class="enter-btn">Open space \u2192</button>
|
||||
<button class="enter-btn">Open space →</button>
|
||||
</div>
|
||||
`;
|
||||
statusBar.style.display = "none";
|
||||
|
|
|
|||
|
|
@ -194,11 +194,11 @@ export class FolkDestination extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\uD83D\uDCCD</span>
|
||||
<span>📍</span>
|
||||
<span>Destination</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dest-body">
|
||||
|
|
|
|||
|
|
@ -239,11 +239,11 @@ export class FolkEmbed extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\u{1F517}</span>
|
||||
<span>🔗</span>
|
||||
<span class="title-text">Embed</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
@ -347,7 +347,7 @@ export class FolkEmbed extends FolkShape {
|
|||
urlInputContainer.innerHTML = `
|
||||
<div class="unsupported">
|
||||
<p>This content cannot be embedded in an iframe.</p>
|
||||
<button class="open-link">Open in new tab \u2192</button>
|
||||
<button class="open-link">Open in new tab →</button>
|
||||
</div>
|
||||
`;
|
||||
const openBtn = urlInputContainer.querySelector(".open-link");
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ export class FolkFreeCAD extends FolkShape {
|
|||
if (this.#previewUrl) {
|
||||
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#previewUrl)}" alt="CAD Preview" />`;
|
||||
} else {
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u2705</span><span>Model generated! Download files below.</span></div>';
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">✅</span><span>Model generated! Download files below.</span></div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ export type GoogleService = "gmail" | "drive" | "photos" | "calendar";
|
|||
export type ItemVisibility = "local" | "shared";
|
||||
|
||||
const SERVICE_ICONS: Record<GoogleService, string> = {
|
||||
gmail: "\u{1F4E7}",
|
||||
drive: "\u{1F4C1}",
|
||||
photos: "\u{1F4F7}",
|
||||
calendar: "\u{1F4C5}",
|
||||
gmail: "📧",
|
||||
drive: "📁",
|
||||
photos: "📷",
|
||||
calendar: "📅",
|
||||
};
|
||||
|
||||
const styles = css`
|
||||
|
|
@ -213,7 +213,7 @@ export class FolkGoogleItem extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="card ${this.#visibility}" data-drag>
|
||||
<span class="visibility-badge" title="Toggle visibility">
|
||||
${this.#visibility === "local" ? "\u{1F512}" : "\u{1F310}"}
|
||||
${this.#visibility === "local" ? "🔒" : "🌐"}
|
||||
</span>
|
||||
<div class="header">
|
||||
<span class="service-icon">${SERVICE_ICONS[this.#service]}</span>
|
||||
|
|
@ -245,7 +245,7 @@ export class FolkGoogleItem extends FolkShape {
|
|||
e.stopPropagation();
|
||||
this.#visibility = this.#visibility === "local" ? "shared" : "local";
|
||||
|
||||
badge.textContent = this.#visibility === "local" ? "\u{1F512}" : "\u{1F310}";
|
||||
badge.textContent = this.#visibility === "local" ? "🔒" : "🌐";
|
||||
card.classList.remove("local", "shared");
|
||||
card.classList.add(this.#visibility);
|
||||
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ export class FolkImageGen extends FolkShape {
|
|||
if (!this.#imageArea) return;
|
||||
this.#imageArea.innerHTML = `
|
||||
<div class="error">${this.#escapeHtml(this.#error || "Unknown error")}</div>
|
||||
${this.#images.length > 0 ? this.#renderImageList() : '<div class="placeholder"><span class="placeholder-icon">\u{1F5BC}</span><span>Try again with a different prompt</span></div>'}
|
||||
${this.#images.length > 0 ? this.#renderImageList() : '<div class="placeholder"><span class="placeholder-icon">🖼</span><span>Try again with a different prompt</span></div>'}
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ export class FolkImageGen extends FolkShape {
|
|||
if (this.#images.length === 0) {
|
||||
this.#imageArea.innerHTML = `
|
||||
<div class="placeholder">
|
||||
<span class="placeholder-icon">\u{1F5BC}</span>
|
||||
<span class="placeholder-icon">🖼</span>
|
||||
<span>Enter a prompt and click Generate</span>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -156,13 +156,13 @@ export interface ItineraryEntry {
|
|||
}
|
||||
|
||||
const CATEGORY_ICONS: Record<string, string> = {
|
||||
FLIGHT: "\u2708\uFE0F",
|
||||
TRANSPORT: "\uD83D\uDE8C",
|
||||
ACCOMMODATION: "\uD83C\uDFE8",
|
||||
ACTIVITY: "\uD83C\uDFAF",
|
||||
MEAL: "\uD83C\uDF7D\uFE0F",
|
||||
FREE_TIME: "\u2600\uFE0F",
|
||||
OTHER: "\uD83D\uDCCC",
|
||||
FLIGHT: "✈️",
|
||||
TRANSPORT: "🚌",
|
||||
ACCOMMODATION: "🏨",
|
||||
ACTIVITY: "🎯",
|
||||
MEAL: "🍽️",
|
||||
FREE_TIME: "☀️",
|
||||
OTHER: "📌",
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
|
@ -222,11 +222,11 @@ export class FolkItinerary extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\uD83D\uDCC5</span>
|
||||
<span>📅</span>
|
||||
<span class="title-text">Itinerary</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="itinerary-container">
|
||||
|
|
|
|||
|
|
@ -434,14 +434,14 @@ export class FolkKiCAD extends FolkShape {
|
|||
if (this.#schematicSvg) {
|
||||
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#schematicSvg)}" alt="Schematic" />`;
|
||||
} else {
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u{1F4CB}</span><span>Schematic will appear here</span></div>';
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">📋</span><span>Schematic will appear here</span></div>';
|
||||
}
|
||||
break;
|
||||
case "board":
|
||||
if (this.#boardSvg) {
|
||||
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#boardSvg)}" alt="Board Layout" />`;
|
||||
} else {
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u{1F4DF}</span><span>Board layout will appear here</span></div>';
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">📟</span><span>Board layout will appear here</span></div>';
|
||||
}
|
||||
break;
|
||||
case "drc":
|
||||
|
|
@ -451,14 +451,14 @@ export class FolkKiCAD extends FolkShape {
|
|||
this.#previewArea.innerHTML = `
|
||||
<div class="drc-results" style="padding:12px">
|
||||
<h4 style="margin:0 0 8px">${passed
|
||||
? '<span class="pass">\u2705 DRC Passed</span>'
|
||||
: `<span class="fail">\u274C ${violations.length} Violation(s)</span>`
|
||||
? '<span class="pass">✅ DRC Passed</span>'
|
||||
: `<span class="fail">❌ ${violations.length} Violation(s)</span>`
|
||||
}</h4>
|
||||
${violations.map((v: any) => `<p style="margin:4px 0;font-size:12px">\u2022 ${this.#escapeHtml(v.message || v)}</p>`).join("")}
|
||||
${violations.map((v: any) => `<p style="margin:4px 0;font-size:12px">• ${this.#escapeHtml(v.message || v)}</p>`).join("")}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u2705</span><span>DRC results will appear here</span></div>';
|
||||
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">✅</span><span>DRC results will appear here</span></div>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,20 +290,20 @@ export class FolkMap extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\u{1F5FA}</span>
|
||||
<span>🗺</span>
|
||||
<span>Map</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<div class="loading">Loading map...</div>
|
||||
<div class="search-box">
|
||||
<input type="text" class="search-input" placeholder="Search location..." />
|
||||
<button class="search-btn">\u{1F50D}</button>
|
||||
<button class="search-btn">🔍</button>
|
||||
</div>
|
||||
<button class="locate-btn" title="My Location">\u{1F4CD}</button>
|
||||
<button class="locate-btn" title="My Location">📍</button>
|
||||
<div class="map"></div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -325,12 +325,12 @@ export class FolkObsNote extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\u{1F4DD}</span>
|
||||
<span>📝</span>
|
||||
<input type="text" class="note-title" placeholder="Note title..." />
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="save-btn" title="Save">\u{1F4BE}</button>
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="save-btn" title="Save">💾</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
@ -340,8 +340,8 @@ export class FolkObsNote extends FolkShape {
|
|||
<button class="toolbar-btn" data-action="italic" title="Italic">I</button>
|
||||
<button class="toolbar-btn" data-action="code" title="Code"></></button>
|
||||
<span class="toolbar-divider"></span>
|
||||
<button class="toolbar-btn" data-action="link" title="Link">\u{1F517}</button>
|
||||
<button class="toolbar-btn" data-action="list" title="List">\u2022</button>
|
||||
<button class="toolbar-btn" data-action="link" title="Link">🔗</button>
|
||||
<button class="toolbar-btn" data-action="list" title="List">•</button>
|
||||
<button class="toolbar-btn" data-action="quote" title="Quote">"</button>
|
||||
<div class="mode-toggle">
|
||||
<button class="mode-btn active" data-mode="edit">Edit</button>
|
||||
|
|
@ -359,7 +359,7 @@ export class FolkObsNote extends FolkShape {
|
|||
<span class="chars">0 characters</span>
|
||||
</div>
|
||||
<div class="save-status saved">
|
||||
<span>\u2713</span>
|
||||
<span>✓</span>
|
||||
<span>Saved</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -609,10 +609,10 @@ export class FolkObsNote extends FolkShape {
|
|||
|
||||
if (this.#isDirty) {
|
||||
this.#saveStatusEl.className = "save-status unsaved";
|
||||
this.#saveStatusEl.innerHTML = "<span>\u2022</span><span>Unsaved</span>";
|
||||
this.#saveStatusEl.innerHTML = "<span>•</span><span>Unsaved</span>";
|
||||
} else {
|
||||
this.#saveStatusEl.className = "save-status saved";
|
||||
this.#saveStatusEl.innerHTML = "<span>\u2713</span><span>Saved</span>";
|
||||
this.#saveStatusEl.innerHTML = "<span>✓</span><span>Saved</span>";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ const styles = css`
|
|||
}
|
||||
|
||||
.checkbox.checked::after {
|
||||
content: "\u2713";
|
||||
content: "✓";
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
|
|
@ -224,11 +224,11 @@ export class FolkPackingList extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\uD83C\uDF92</span>
|
||||
<span>🎒</span>
|
||||
<span>Packing List</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-info">
|
||||
|
|
|
|||
|
|
@ -171,16 +171,16 @@ export class FolkPiano extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="piano-container" data-drag>
|
||||
<div class="loading">
|
||||
<span>\u{1F3B9}</span>
|
||||
<span>🎹</span>
|
||||
<span>Loading Shared Piano...</span>
|
||||
</div>
|
||||
<div class="error hidden">
|
||||
<span>\u{1F3B9}</span>
|
||||
<span>🎹</span>
|
||||
<span class="error-message">Failed to load piano</span>
|
||||
<button class="retry-btn">Retry</button>
|
||||
</div>
|
||||
<div class="minimized hidden">
|
||||
<span>\u{1F3B9} Shared Piano</span>
|
||||
<span>🎹 Shared Piano</span>
|
||||
</div>
|
||||
<iframe
|
||||
class="piano-iframe"
|
||||
|
|
@ -190,7 +190,7 @@ export class FolkPiano extends FolkShape {
|
|||
style="opacity: 0;"
|
||||
></iframe>
|
||||
<div class="controls">
|
||||
<button class="control-btn minimize-btn" title="Minimize">\u{1F53C}</button>
|
||||
<button class="control-btn minimize-btn" title="Minimize">🔼</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -231,14 +231,14 @@ export class FolkPiano extends FolkShape {
|
|||
minimizeBtn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.isMinimized = !this.#isMinimized;
|
||||
minimizeBtn.textContent = this.#isMinimized ? "\u{1F53D}" : "\u{1F53C}";
|
||||
minimizeBtn.textContent = this.#isMinimized ? "🔽" : "🔼";
|
||||
});
|
||||
|
||||
// Click minimized view to expand
|
||||
this.#minimizedEl?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.isMinimized = false;
|
||||
if (minimizeBtn) minimizeBtn.textContent = "\u{1F53C}";
|
||||
if (minimizeBtn) minimizeBtn.textContent = "🔼";
|
||||
});
|
||||
|
||||
// Retry button
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const styles = css`
|
|||
}
|
||||
|
||||
.message.streaming::after {
|
||||
content: "\u258C";
|
||||
content: "▌";
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
|
|
@ -262,18 +262,18 @@ export class FolkPrompt extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\u{1F4AC}</span>
|
||||
<span>💬</span>
|
||||
<span>AI Prompt</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="clear-btn" title="Clear chat">Clear</button>
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="messages">
|
||||
<div class="placeholder">
|
||||
<span class="placeholder-icon">\u{1F916}</span>
|
||||
<span class="placeholder-icon">🤖</span>
|
||||
<span>Ask me anything!</span>
|
||||
<span style="font-size: 11px;">I can help with code, writing, analysis, and more</span>
|
||||
</div>
|
||||
|
|
@ -287,7 +287,7 @@ export class FolkPrompt extends FolkShape {
|
|||
</select>
|
||||
<div class="prompt-row">
|
||||
<textarea class="prompt-input" placeholder="Type your message..." rows="2"></textarea>
|
||||
<button class="send-btn">\u2192</button>
|
||||
<button class="send-btn">→</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -418,7 +418,7 @@ export class FolkPrompt extends FolkShape {
|
|||
if (this.#messages.length === 0 && !this.#error) {
|
||||
this.#messagesEl.innerHTML = `
|
||||
<div class="placeholder">
|
||||
<span class="placeholder-icon">\u{1F916}</span>
|
||||
<span class="placeholder-icon">🤖</span>
|
||||
<span>Ask me anything!</span>
|
||||
<span style="font-size: 11px;">I can help with code, writing, analysis, and more</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -363,6 +363,16 @@ export class FolkShape extends FolkElement {
|
|||
return this.#readonlyRect;
|
||||
}
|
||||
|
||||
/** Compute the effective CSS transform scale of the parent (e.g. canvas zoom). */
|
||||
#getParentScale(): number {
|
||||
const parent = this.parentElement;
|
||||
if (!parent) return 1;
|
||||
const rect = parent.getBoundingClientRect();
|
||||
const w = parent.offsetWidth;
|
||||
if (!w || !rect.width) return 1;
|
||||
return rect.width / w;
|
||||
}
|
||||
|
||||
handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
|
||||
// Handle touch events for mobile drag support
|
||||
if (event instanceof TouchEvent) {
|
||||
|
|
@ -390,7 +400,7 @@ export class FolkShape extends FolkElement {
|
|||
if (event.type === "touchmove" && this.#isTouchDragging && event.touches.length === 1) {
|
||||
const touch = event.touches[0];
|
||||
if (this.#lastTouchPos) {
|
||||
const zoom = window.visualViewport?.scale ?? 1;
|
||||
const zoom = (window.visualViewport?.scale ?? 1) * this.#getParentScale();
|
||||
const moveDelta = {
|
||||
x: (touch.clientX - this.#lastTouchPos.x) / zoom,
|
||||
y: (touch.clientY - this.#lastTouchPos.y) / zoom,
|
||||
|
|
@ -478,7 +488,7 @@ export class FolkShape extends FolkElement {
|
|||
};
|
||||
} else if (event.type === "pointermove") {
|
||||
if (!target) return;
|
||||
const zoom = window.visualViewport?.scale ?? 1;
|
||||
const zoom = (window.visualViewport?.scale ?? 1) * this.#getParentScale();
|
||||
moveDelta = {
|
||||
x: event.movementX / zoom,
|
||||
y: event.movementY / zoom,
|
||||
|
|
@ -659,9 +669,10 @@ export class FolkShape extends FolkElement {
|
|||
|
||||
/**
|
||||
* After moving, push this shape away from any overlapping siblings.
|
||||
* Resolves by minimum penetration on the axis aligned with movement direction.
|
||||
* Uses minimum penetration depth — picks the smallest displacement
|
||||
* among all four directions to resolve each overlap.
|
||||
*/
|
||||
#resolveOverlaps(dx: number, dy: number) {
|
||||
#resolveOverlaps(_dx: number, _dy: number) {
|
||||
const parent = this.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
|
|
@ -680,21 +691,25 @@ export class FolkShape extends FolkElement {
|
|||
|
||||
if (!overlapX || !overlapY) continue;
|
||||
|
||||
// Distance to clear on each side
|
||||
const clearRight = (other.x + other.w + gap) - me.x; // push me right of other
|
||||
const clearLeft = other.x - (me.x + me.w + gap); // push me left of other (negative)
|
||||
const clearDown = (other.y + other.h + gap) - me.y; // push me below other
|
||||
const clearUp = other.y - (me.y + me.h + gap); // push me above other (negative)
|
||||
// Distance to clear on each side (4 possible escape directions)
|
||||
const clearRight = (other.x + other.w + gap) - me.x;
|
||||
const clearLeft = other.x - (me.x + me.w + gap);
|
||||
const clearDown = (other.y + other.h + gap) - me.y;
|
||||
const clearUp = other.y - (me.y + me.h + gap);
|
||||
|
||||
// Pick push direction per axis based on movement direction
|
||||
const pushX = dx >= 0 ? clearRight : clearLeft;
|
||||
const pushY = dy >= 0 ? clearDown : clearUp;
|
||||
// Pick the direction with smallest absolute displacement
|
||||
const candidates = [
|
||||
{ axis: "x" as const, d: clearRight },
|
||||
{ axis: "x" as const, d: clearLeft },
|
||||
{ axis: "y" as const, d: clearDown },
|
||||
{ axis: "y" as const, d: clearUp },
|
||||
];
|
||||
const best = candidates.reduce((a, b) => Math.abs(a.d) < Math.abs(b.d) ? a : b);
|
||||
|
||||
// Apply the axis with the smallest absolute displacement
|
||||
if (Math.abs(pushX) <= Math.abs(pushY)) {
|
||||
this.#rect.x += pushX;
|
||||
if (best.axis === "x") {
|
||||
this.#rect.x += best.d;
|
||||
} else {
|
||||
this.#rect.y += pushY;
|
||||
this.#rect.y += best.d;
|
||||
}
|
||||
|
||||
me.x = this.#rect.x;
|
||||
|
|
|
|||
|
|
@ -370,13 +370,13 @@ const PLATFORM_CONFIG: Record<
|
|||
SocialPlatform,
|
||||
{ icon: string; label: string; color: string }
|
||||
> = {
|
||||
x: { icon: "\ud835\udd4f", label: "X", color: "#000000" },
|
||||
x: { icon: "𝕏", label: "X", color: "#000000" },
|
||||
linkedin: { icon: "in", label: "LinkedIn", color: "#0A66C2" },
|
||||
instagram: { icon: "\ud83d\udcf7", label: "Instagram", color: "#E4405F" },
|
||||
youtube: { icon: "\u25b6", label: "YouTube", color: "#FF0000" },
|
||||
instagram: { icon: "📷", label: "Instagram", color: "#E4405F" },
|
||||
youtube: { icon: "▶", label: "YouTube", color: "#FF0000" },
|
||||
threads: { icon: "@", label: "Threads", color: "#000000" },
|
||||
bluesky: { icon: "\ud83e\ude77", label: "Bluesky", color: "#0085FF" },
|
||||
tiktok: { icon: "\u266b", label: "TikTok", color: "#010101" },
|
||||
bluesky: { icon: "🩷", label: "Bluesky", color: "#0085FF" },
|
||||
tiktok: { icon: "♫", label: "TikTok", color: "#010101" },
|
||||
facebook: { icon: "f", label: "Facebook", color: "#1877F2" },
|
||||
};
|
||||
|
||||
|
|
@ -561,8 +561,8 @@ export class FolkSocialPost extends FolkShape {
|
|||
<span class="platform-name">${config.label}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="edit-btn" title="Edit">\u270F</button>
|
||||
<button class="close-btn" title="Remove">\u00D7</button>
|
||||
<button class="edit-btn" title="Edit">✏</button>
|
||||
<button class="close-btn" title="Remove">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
|
|
@ -572,7 +572,7 @@ export class FolkSocialPost extends FolkShape {
|
|||
</div>
|
||||
<div class="media-preview">
|
||||
<div class="media-placeholder">
|
||||
<span class="icon">\ud83d\uddbc</span>
|
||||
<span class="icon">🖼</span>
|
||||
<span>No media</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -580,7 +580,7 @@ export class FolkSocialPost extends FolkShape {
|
|||
</div>
|
||||
<div class="footer">
|
||||
<div class="schedule">
|
||||
<span class="schedule-icon">\ud83d\udcc5</span>
|
||||
<span class="schedule-icon">📅</span>
|
||||
<span class="schedule-time"
|
||||
>${this.#formatSchedule(this.#scheduledAt)}</span
|
||||
>
|
||||
|
|
@ -820,14 +820,14 @@ export class FolkSocialPost extends FolkShape {
|
|||
this.#mediaPreviewEl.innerHTML = `<img src="${this.#escapeHtml(this.#mediaUrl)}" alt="Post media" onerror="this.parentElement.innerHTML='<div class=\\'media-placeholder\\'><span class=\\'icon\\'>⚠️</span><span>Failed to load</span></div>'" />`;
|
||||
} else {
|
||||
const mediaIcons: Record<string, string> = {
|
||||
image: "\ud83d\uddbc",
|
||||
video: "\ud83c\udfac",
|
||||
carousel: "\ud83d\udcf8",
|
||||
reel: "\ud83c\udfac",
|
||||
short: "\ud83c\udfac",
|
||||
story: "\ud83d\udcf1",
|
||||
image: "🖼",
|
||||
video: "🎬",
|
||||
carousel: "📸",
|
||||
reel: "🎬",
|
||||
short: "🎬",
|
||||
story: "📱",
|
||||
};
|
||||
const icon = mediaIcons[this.#postType] || "\ud83d\uddbc";
|
||||
const icon = mediaIcons[this.#postType] || "🖼";
|
||||
const label =
|
||||
this.#postType === "text" ? "No media" : `${this.#postType} media`;
|
||||
this.#mediaPreviewEl.innerHTML = `<div class="media-placeholder"><span class="icon">${icon}</span><span>${label}</span></div>`;
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ export class FolkSplat extends FolkShape {
|
|||
container.innerHTML = this.#gallerySplats.map((s) => `
|
||||
<div class="gallery-item" data-slug="${s.slug}">
|
||||
<div class="title">${this.#escapeHtml(s.title)}</div>
|
||||
<div class="meta">${s.file_format} \u2022 ${(s.file_size_bytes / 1024 / 1024).toFixed(1)}MB</div>
|
||||
<div class="meta">${s.file_format} • ${(s.file_size_bytes / 1024 / 1024).toFixed(1)}MB</div>
|
||||
</div>
|
||||
`).join("");
|
||||
|
||||
|
|
|
|||
|
|
@ -302,12 +302,12 @@ export class FolkTokenLedger extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\uD83D\uDCDC</span>
|
||||
<span>📜</span>
|
||||
<span class="name">Token Ledger</span>
|
||||
<span class="symbol"></span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ledger-body">
|
||||
|
|
@ -470,7 +470,7 @@ export class FolkTokenLedger extends FolkShape {
|
|||
<div class="holder-item">
|
||||
<div class="holder-info">
|
||||
<div class="holder-icon" style="background: ${info.isEscrow ? '#f59e0b' : this.#holderColor(holder)}">
|
||||
${info.isEscrow ? '\u2709' : info.label.charAt(0).toUpperCase()}
|
||||
${info.isEscrow ? '✉' : info.label.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div class="holder-name">
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ export class FolkTokenMint extends FolkShape {
|
|||
#totalSupply = 1000;
|
||||
#issuedSupply = 0;
|
||||
#tokenColor = "#8b5cf6";
|
||||
#tokenIcon = "\uD83E\uDE99";
|
||||
#tokenIcon = "🪙";
|
||||
#createdBy = "";
|
||||
#createdAt = new Date().toISOString();
|
||||
|
||||
|
|
@ -243,7 +243,7 @@ export class FolkTokenMint extends FolkShape {
|
|||
<span class="symbol">${this.#tokenSymbol}</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mint-body">
|
||||
|
|
|
|||
|
|
@ -318,11 +318,11 @@ export class FolkTranscription extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\u{1F3A4}</span>
|
||||
<span>🎤</span>
|
||||
<span>Transcription</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
@ -337,14 +337,14 @@ export class FolkTranscription extends FolkShape {
|
|||
</div>
|
||||
<div class="transcript-area">
|
||||
<div class="placeholder">
|
||||
<span class="placeholder-icon">\u{1F3A4}</span>
|
||||
<span class="placeholder-icon">🎤</span>
|
||||
<span>Click the record button to start</span>
|
||||
<span style="font-size: 11px;">Uses your browser's speech recognition</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="action-btn copy-btn">\u{1F4CB} Copy</button>
|
||||
<button class="action-btn clear-btn">\u{1F5D1} Clear</button>
|
||||
<button class="action-btn copy-btn">📋 Copy</button>
|
||||
<button class="action-btn clear-btn">🗑 Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -544,7 +544,7 @@ export class FolkTranscription extends FolkShape {
|
|||
if (this.#segments.length === 0) {
|
||||
this.#transcriptArea.innerHTML = `
|
||||
<div class="placeholder">
|
||||
<span class="placeholder-icon">\u{1F3A4}</span>
|
||||
<span class="placeholder-icon">🎤</span>
|
||||
<span>Click the record button to start</span>
|
||||
<span style="font-size: 11px;">Uses your browser's speech recognition</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -307,16 +307,16 @@ export class FolkVideoChat extends FolkShape {
|
|||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span>\u{1F4F9}</span>
|
||||
<span>📹</span>
|
||||
<span>Video Chat</span>
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="join-screen">
|
||||
<span class="join-icon">\u{1F4F9}</span>
|
||||
<span class="join-icon">📹</span>
|
||||
<span class="join-title">Join Video Call</span>
|
||||
<span class="join-subtitle">Enter a room name to start or join a call</span>
|
||||
<input type="text" class="room-input" placeholder="Room name..." />
|
||||
|
|
@ -402,10 +402,10 @@ export class FolkVideoChat extends FolkShape {
|
|||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button class="control-btn secondary" id="mute-btn" title="Mute">\u{1F50A}</button>
|
||||
<button class="control-btn secondary" id="video-btn" title="Toggle Video">\u{1F4F7}</button>
|
||||
<button class="control-btn secondary" id="record-btn" title="Record">\u{23FA}</button>
|
||||
<button class="control-btn danger" id="leave-btn" title="Leave Call">\u{1F4DE}</button>
|
||||
<button class="control-btn secondary" id="mute-btn" title="Mute">🔊</button>
|
||||
<button class="control-btn secondary" id="video-btn" title="Toggle Video">📷</button>
|
||||
<button class="control-btn secondary" id="record-btn" title="Record">⏺</button>
|
||||
<button class="control-btn danger" id="leave-btn" title="Leave Call">📞</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -464,7 +464,7 @@ export class FolkVideoChat extends FolkShape {
|
|||
track.enabled = !this.#isMuted;
|
||||
});
|
||||
}
|
||||
btn.textContent = this.#isMuted ? "\u{1F507}" : "\u{1F50A}";
|
||||
btn.textContent = this.#isMuted ? "🔇" : "🔊";
|
||||
btn.classList.toggle("muted", this.#isMuted);
|
||||
}
|
||||
|
||||
|
|
@ -475,7 +475,7 @@ export class FolkVideoChat extends FolkShape {
|
|||
track.enabled = !this.#isVideoOff;
|
||||
});
|
||||
}
|
||||
btn.textContent = this.#isVideoOff ? "\u{1F4F7}\u{FE0F}\u{20E0}" : "\u{1F4F7}";
|
||||
btn.textContent = this.#isVideoOff ? "📷️⃠" : "📷";
|
||||
btn.classList.toggle("muted", this.#isVideoOff);
|
||||
video.style.opacity = this.#isVideoOff ? "0.3" : "1";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ export class FolkVideoGen extends FolkShape {
|
|||
if (!this.#videoArea) return;
|
||||
this.#videoArea.innerHTML = `
|
||||
<div class="error">${this.#escapeHtml(this.#error || "Unknown error")}</div>
|
||||
${this.#videos.length > 0 ? this.#renderVideoList() : '<div class="placeholder"><span class="placeholder-icon">\u{1F3AC}</span><span>Try again</span></div>'}
|
||||
${this.#videos.length > 0 ? this.#renderVideoList() : '<div class="placeholder"><span class="placeholder-icon">🎬</span><span>Try again</span></div>'}
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
@ -523,7 +523,7 @@ export class FolkVideoGen extends FolkShape {
|
|||
if (this.#videos.length === 0) {
|
||||
this.#videoArea.innerHTML = `
|
||||
<div class="placeholder">
|
||||
<span class="placeholder-icon">\u{1F3AC}</span>
|
||||
<span class="placeholder-icon">🎬</span>
|
||||
<span>Upload an image and describe the motion</span>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ export class FolkWorkflowBlock extends FolkShape {
|
|||
|
||||
#blockType: BlockType = "action";
|
||||
#label = "Block";
|
||||
#icon = "\u{2699}";
|
||||
#icon = "⚙";
|
||||
#state: BlockState = "idle";
|
||||
#inputs: Port[] = [];
|
||||
#outputs: Port[] = [];
|
||||
|
|
@ -324,8 +324,8 @@ export class FolkWorkflowBlock extends FolkShape {
|
|||
<span class="block-label">${this.#escapeHtml(this.#label)}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="settings-btn" title="Settings">\u{2699}</button>
|
||||
<button class="close-btn" title="Close">\u00D7</button>
|
||||
<button class="settings-btn" title="Settings">⚙</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
@ -336,7 +336,7 @@ export class FolkWorkflowBlock extends FolkShape {
|
|||
<span class="status-dot idle"></span>
|
||||
<span class="status-text">Idle</span>
|
||||
</div>
|
||||
<button class="run-btn">\u25B6 Run</button>
|
||||
<button class="run-btn">▶ Run</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -382,16 +382,16 @@ export class FolkWorkflowBlock extends FolkShape {
|
|||
#updateIcon() {
|
||||
switch (this.#blockType) {
|
||||
case "trigger":
|
||||
this.#icon = "\u26A1";
|
||||
this.#icon = "⚡";
|
||||
break;
|
||||
case "action":
|
||||
this.#icon = "\u2699";
|
||||
this.#icon = "⚙";
|
||||
break;
|
||||
case "condition":
|
||||
this.#icon = "\u2753";
|
||||
this.#icon = "❓";
|
||||
break;
|
||||
case "output":
|
||||
this.#icon = "\u{1F4E4}";
|
||||
this.#icon = "📤";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ export class FolkBookReader extends HTMLElement {
|
|||
${this.getStyles()}
|
||||
<div class="reader-container">
|
||||
<div class="rapp-nav">
|
||||
<a class="rapp-nav__back" href="/${window.location.pathname.split('/')[1]}/rbooks">\u2190 Library</a>
|
||||
<a class="rapp-nav__back" href="/${window.location.pathname.split('/')[1]}/rbooks">← Library</a>
|
||||
<span class="rapp-nav__title">${this.escapeHtml(this._title)}</span>
|
||||
${this._author ? `<span class="rapp-nav__subtitle">by ${this.escapeHtml(this._author)}</span>` : ""}
|
||||
<span class="rapp-nav__meta">
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ class FolkCalendarView extends HTMLElement {
|
|||
|
||||
private getMoonEmoji(phase: string): string {
|
||||
const map: Record<string, string> = {
|
||||
new_moon: "\u{1F311}", waxing_crescent: "\u{1F312}", first_quarter: "\u{1F313}",
|
||||
waxing_gibbous: "\u{1F314}", full_moon: "\u{1F315}", waning_gibbous: "\u{1F316}",
|
||||
last_quarter: "\u{1F317}", waning_crescent: "\u{1F318}",
|
||||
new_moon: "🌑", waxing_crescent: "🌒", first_quarter: "🌓",
|
||||
waxing_gibbous: "🌔", full_moon: "🌕", waning_gibbous: "🌖",
|
||||
last_quarter: "🌗", waning_crescent: "🌘",
|
||||
};
|
||||
return map[phase] || "";
|
||||
}
|
||||
|
|
@ -120,10 +120,10 @@ class FolkCalendarView extends HTMLElement {
|
|||
${this.error ? `<div style="color:#ef5350;text-align:center;padding:8px">${this.esc(this.error)}</div>` : ""}
|
||||
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" id="prev">\u2190</button>
|
||||
<button class="rapp-nav__back" id="prev">←</button>
|
||||
<span class="rapp-nav__title">${monthName} ${year}</span>
|
||||
<button class="toggle-btn ${this.showLunar ? "active" : ""}" id="toggle-lunar">\u{1F319} Lunar</button>
|
||||
<button class="rapp-nav__back" id="next">\u2192</button>
|
||||
<button class="toggle-btn ${this.showLunar ? "active" : ""}" id="toggle-lunar">🌙 Lunar</button>
|
||||
<button class="rapp-nav__back" id="next">→</button>
|
||||
</div>
|
||||
|
||||
${this.sources.length > 0 ? `<div class="sources">
|
||||
|
|
@ -191,10 +191,10 @@ class FolkCalendarView extends HTMLElement {
|
|||
return `
|
||||
<div class="event-modal" id="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close" id="modal-close">\u2715</button>
|
||||
<button class="modal-close" id="modal-close">✕</button>
|
||||
<div class="modal-title">${this.esc(e.title)}</div>
|
||||
${e.description ? `<div class="modal-field">${this.esc(e.description)}</div>` : ""}
|
||||
<div class="modal-field">When: ${new Date(e.start_time).toLocaleString()}${e.end_time ? ` \u2014 ${new Date(e.end_time).toLocaleString()}` : ""}</div>
|
||||
<div class="modal-field">When: ${new Date(e.start_time).toLocaleString()}${e.end_time ? ` — ${new Date(e.end_time).toLocaleString()}` : ""}</div>
|
||||
${e.location_name ? `<div class="modal-field">Where: ${this.esc(e.location_name)}</div>` : ""}
|
||||
${e.source_name ? `<div class="modal-field">Source: ${this.esc(e.source_name)}</div>` : ""}
|
||||
${e.is_virtual ? `<div class="modal-field">Virtual: ${this.esc(e.virtual_platform || "")} ${e.virtual_url ? `<a href="${e.virtual_url}" target="_blank" style="color:#6366f1">Join</a>` : ""}</div>` : ""}
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ routes.get("/", (c) => {
|
|||
export const calModule: RSpaceModule = {
|
||||
id: "rcal",
|
||||
name: "rCal",
|
||||
icon: "\u{1F4C5}",
|
||||
icon: "📅",
|
||||
description: "Temporal coordination calendar with lunar, solar, and seasonal systems",
|
||||
routes,
|
||||
standaloneDomain: "rcal.online",
|
||||
|
|
|
|||
|
|
@ -80,12 +80,12 @@ class FolkCartShop extends HTMLElement {
|
|||
<div class="rapp-nav">
|
||||
<span class="rapp-nav__title">Shop</span>
|
||||
<div class="tabs">
|
||||
<button class="tab ${this.view === 'catalog' ? 'active' : ''}" data-view="catalog">\u{1F4E6} Catalog (${this.catalog.length})</button>
|
||||
<button class="tab ${this.view === 'orders' ? 'active' : ''}" data-view="orders">\u{1F4CB} Orders (${this.orders.length})</button>
|
||||
<button class="tab ${this.view === 'catalog' ? 'active' : ''}" data-view="catalog">📦 Catalog (${this.catalog.length})</button>
|
||||
<button class="tab ${this.view === 'orders' ? 'active' : ''}" data-view="orders">📋 Orders (${this.orders.length})</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.loading ? `<div class="loading">\u23F3 Loading...</div>` :
|
||||
${this.loading ? `<div class="loading">⏳ Loading...</div>` :
|
||||
this.view === "catalog" ? this.renderCatalog() : this.renderOrders()}
|
||||
`;
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ class FolkCartShop extends HTMLElement {
|
|||
<h3 class="card-title">${this.esc(order.artifact_title || "Order")}</h3>
|
||||
<div class="card-meta">
|
||||
${order.provider_name ? `Provider: ${this.esc(order.provider_name)}` : ""}
|
||||
${order.quantity > 1 ? ` \u2022 Qty: ${order.quantity}` : ""}
|
||||
${order.quantity > 1 ? ` • Qty: ${order.quantity}` : ""}
|
||||
</div>
|
||||
<span class="status status-${order.status}">${order.status}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@ routes.get("/", (c) => {
|
|||
export const cartModule: RSpaceModule = {
|
||||
id: "rcart",
|
||||
name: "rCart",
|
||||
icon: "\u{1F6D2}",
|
||||
icon: "🛒",
|
||||
description: "Cosmolocal print-on-demand shop",
|
||||
routes,
|
||||
standaloneDomain: "rcart.online",
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ class FolkChoicesDashboard extends HTMLElement {
|
|||
|
||||
private render() {
|
||||
const typeIcons: Record<string, string> = {
|
||||
"folk-choice-vote": "\u2611",
|
||||
"folk-choice-rank": "\uD83D\uDCCA",
|
||||
"folk-choice-spider": "\uD83D\uDD78",
|
||||
"folk-choice-vote": "☑",
|
||||
"folk-choice-rank": "📊",
|
||||
"folk-choice-spider": "🕸",
|
||||
};
|
||||
const typeLabels: Record<string, string> = {
|
||||
"folk-choice-vote": "Poll",
|
||||
|
|
@ -78,7 +78,7 @@ class FolkChoicesDashboard extends HTMLElement {
|
|||
<div class="rapp-nav">
|
||||
<span class="rapp-nav__title">Choices</span>
|
||||
<div class="create-btns">
|
||||
<a class="create-btn" href="/${this.space}/rspace" title="Open canvas to create choices">\u2795 New on Canvas</a>
|
||||
<a class="create-btn" href="/${this.space}/rspace" title="Open canvas to create choices">➕ New on Canvas</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -87,14 +87,14 @@ class FolkChoicesDashboard extends HTMLElement {
|
|||
Create them there and they'll appear here for quick access.
|
||||
</div>
|
||||
|
||||
${this.loading ? `<div class="loading">\u23F3 Loading choices...</div>` :
|
||||
${this.loading ? `<div class="loading">⏳ Loading choices...</div>` :
|
||||
this.choices.length === 0 ? this.renderEmpty() : this.renderGrid(typeIcons, typeLabels)}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmpty(): string {
|
||||
return `<div class="empty">
|
||||
<div class="empty-icon">\u2611</div>
|
||||
<div class="empty-icon">☑</div>
|
||||
<p>No choices in this space yet.</p>
|
||||
<p>Open the <a href="/${this.space}/rspace" style="color:#818cf8">canvas</a> and use the Poll, Rank, or Spider buttons to create one.</p>
|
||||
</div>`;
|
||||
|
|
@ -104,7 +104,7 @@ class FolkChoicesDashboard extends HTMLElement {
|
|||
return `<div class="grid">
|
||||
${this.choices.map((ch) => `
|
||||
<a class="card" href="/${this.space}/rspace">
|
||||
<div class="card-icon">${icons[ch.type] || "\u2611"}</div>
|
||||
<div class="card-icon">${icons[ch.type] || "☑"}</div>
|
||||
<div class="card-type">${labels[ch.type] || ch.type}</div>
|
||||
<h3 class="card-title">${this.esc(ch.title)}</h3>
|
||||
<div class="card-meta">
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ routes.get("/", (c) => {
|
|||
export const dataModule: RSpaceModule = {
|
||||
id: "rdata",
|
||||
name: "rData",
|
||||
icon: "\u{1F4CA}",
|
||||
icon: "📊",
|
||||
description: "Privacy-first analytics for the r* ecosystem",
|
||||
routes,
|
||||
standaloneDomain: "rdata.online",
|
||||
|
|
|
|||
|
|
@ -69,24 +69,24 @@ class FolkFileBrowser extends HTMLElement {
|
|||
}
|
||||
|
||||
private mimeIcon(mime: string): string {
|
||||
if (mime?.startsWith("image/")) return "\uD83D\uDDBC\uFE0F";
|
||||
if (mime?.startsWith("video/")) return "\uD83C\uDFA5";
|
||||
if (mime?.startsWith("audio/")) return "\uD83C\uDFB5";
|
||||
if (mime?.includes("pdf")) return "\uD83D\uDCC4";
|
||||
if (mime?.includes("zip") || mime?.includes("tar") || mime?.includes("gz")) return "\uD83D\uDCE6";
|
||||
if (mime?.includes("text") || mime?.includes("json") || mime?.includes("xml")) return "\uD83D\uDCDD";
|
||||
return "\uD83D\uDCC1";
|
||||
if (mime?.startsWith("image/")) return "🖼️";
|
||||
if (mime?.startsWith("video/")) return "🎥";
|
||||
if (mime?.startsWith("audio/")) return "🎵";
|
||||
if (mime?.includes("pdf")) return "📄";
|
||||
if (mime?.includes("zip") || mime?.includes("tar") || mime?.includes("gz")) return "📦";
|
||||
if (mime?.includes("text") || mime?.includes("json") || mime?.includes("xml")) return "📝";
|
||||
return "📁";
|
||||
}
|
||||
|
||||
private cardTypeIcon(type: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
note: "\uD83D\uDCDD",
|
||||
idea: "\uD83D\uDCA1",
|
||||
task: "\u2705",
|
||||
reference: "\uD83D\uDD17",
|
||||
quote: "\uD83D\uDCAC",
|
||||
note: "📝",
|
||||
idea: "💡",
|
||||
task: "✅",
|
||||
reference: "🔗",
|
||||
quote: "💬",
|
||||
};
|
||||
return icons[type] || "\uD83D\uDCDD";
|
||||
return icons[type] || "📝";
|
||||
}
|
||||
|
||||
private async handleUpload(e: Event) {
|
||||
|
|
@ -258,8 +258,8 @@ class FolkFileBrowser extends HTMLElement {
|
|||
</style>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab-btn" data-tab="files">\uD83D\uDCC1 Files</div>
|
||||
<div class="tab-btn" data-tab="cards">\uD83C\uDFB4 Memory Cards</div>
|
||||
<div class="tab-btn" data-tab="files">📁 Files</div>
|
||||
<div class="tab-btn" data-tab="cards">🎴 Memory Cards</div>
|
||||
</div>
|
||||
|
||||
${filesActive ? this.renderFilesTab() : this.renderCardsTab()}
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@ routes.get("/", (c) => {
|
|||
export const filesModule: RSpaceModule = {
|
||||
id: "rfiles",
|
||||
name: "rFiles",
|
||||
icon: "\uD83D\uDCC1",
|
||||
icon: "📁",
|
||||
description: "File sharing, share links, and memory cards",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -150,10 +150,10 @@ class FolkForumDashboard extends HTMLElement {
|
|||
}
|
||||
|
||||
private logStepIcon(status: string): string {
|
||||
if (status === "success") return "\u2705";
|
||||
if (status === "error") return "\u274C";
|
||||
if (status === "running") return "\u23F3";
|
||||
return "\u23ED\uFE0F";
|
||||
if (status === "success") return "✅";
|
||||
if (status === "error") return "❌";
|
||||
if (status === "running") return "⏳";
|
||||
return "⏭️";
|
||||
}
|
||||
|
||||
private render() {
|
||||
|
|
@ -264,7 +264,7 @@ class FolkForumDashboard extends HTMLElement {
|
|||
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-action="back">\u2190 Forums</button>
|
||||
<button class="rapp-nav__back" data-action="back">← Forums</button>
|
||||
<span class="rapp-nav__title">${this.esc(inst.name)}</span>
|
||||
${inst.status !== "destroyed" ? `<button class="danger" data-action="destroy" data-id="${inst.id}">Destroy</button>` : ""}
|
||||
</div>
|
||||
|
|
@ -275,7 +275,7 @@ class FolkForumDashboard extends HTMLElement {
|
|||
<div class="detail-title">${this.esc(inst.name)}</div>
|
||||
<div style="margin-top:4px">${this.statusBadge(inst.status)}</div>
|
||||
</div>
|
||||
${inst.status === "active" ? `<a href="https://${inst.domain}" target="_blank" style="color:#64b5f6;font-size:13px">\u2197 Open Forum</a>` : ""}
|
||||
${inst.status === "active" ? `<a href="https://${inst.domain}" target="_blank" style="color:#64b5f6;font-size:13px">↗ Open Forum</a>` : ""}
|
||||
</div>
|
||||
|
||||
${inst.error_message ? `<div style="background:#7a2a2a33;border:1px solid #7a2a2a;padding:10px;border-radius:6px;margin-bottom:16px;font-size:13px;color:#ef5350">${this.esc(inst.error_message)}</div>` : ""}
|
||||
|
|
@ -286,7 +286,7 @@ class FolkForumDashboard extends HTMLElement {
|
|||
<div class="detail-item"><label>Region</label><div class="value">${inst.region}</div></div>
|
||||
<div class="detail-item"><label>Server Size</label><div class="value">${inst.size}</div></div>
|
||||
<div class="detail-item"><label>Admin Email</label><div class="value">${inst.admin_email || "—"}</div></div>
|
||||
<div class="detail-item"><label>SSL</label><div class="value">${inst.ssl_provisioned ? "\u2705 Active" : "\u23F3 Pending"}</div></div>
|
||||
<div class="detail-item"><label>SSL</label><div class="value">${inst.ssl_provisioned ? "✅ Active" : "⏳ Pending"}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="logs-section">
|
||||
|
|
@ -309,7 +309,7 @@ class FolkForumDashboard extends HTMLElement {
|
|||
private renderCreate(): string {
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-action="back">\u2190 Forums</button>
|
||||
<button class="rapp-nav__back" data-action="back">← Forums</button>
|
||||
<span class="rapp-nav__title">Deploy New Forum</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -319,17 +319,17 @@ class FolkForumDashboard extends HTMLElement {
|
|||
<div class="pricing">
|
||||
<div class="price-card selected" data-size="cx22">
|
||||
<div class="price-name">Starter</div>
|
||||
<div class="price-cost">\u20AC3.79/mo</div>
|
||||
<div class="price-cost">€3.79/mo</div>
|
||||
<div class="price-specs">2 vCPU · 4 GB · ~500 users</div>
|
||||
</div>
|
||||
<div class="price-card" data-size="cx32">
|
||||
<div class="price-name">Standard</div>
|
||||
<div class="price-cost">\u20AC6.80/mo</div>
|
||||
<div class="price-cost">€6.80/mo</div>
|
||||
<div class="price-specs">4 vCPU · 8 GB · ~2000 users</div>
|
||||
</div>
|
||||
<div class="price-card" data-size="cx42">
|
||||
<div class="price-name">Performance</div>
|
||||
<div class="price-cost">\u20AC13.80/mo</div>
|
||||
<div class="price-cost">€13.80/mo</div>
|
||||
<div class="price-specs">8 vCPU · 16 GB · ~10k users</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ routes.get("/", (c) => {
|
|||
export const forumModule: RSpaceModule = {
|
||||
id: "rforum",
|
||||
name: "rForum",
|
||||
icon: "\uD83D\uDCAC",
|
||||
icon: "💬",
|
||||
description: "Deploy and manage Discourse forums",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ function renderFunnel(f: FunnelLayout): string {
|
|||
<rect x="${f.x}" y="${f.y}" width="${f.segmentLength}" height="${f.riverWidth}" rx="4" fill="url(#${gradId})"/>
|
||||
${[0, 1, 2].map((i) => `<rect x="${f.x}" y="${f.y + (f.riverWidth / 4) * i}" width="${f.segmentLength}" height="${f.riverWidth / 4}" fill="${colors[0]}" opacity="0.08" style="animation:waterFlow ${2 + i * 0.5}s linear infinite;animation-delay:${i * -0.6}s"/>`).join("")}
|
||||
<text x="${f.x + f.segmentLength / 2}" y="${f.y - 12}" text-anchor="middle" fill="${COLORS.text}" font-size="13" font-weight="600">${esc(f.label)}</text>
|
||||
<text x="${f.x + f.segmentLength / 2}" y="${f.y - 2}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">$${Math.floor(f.data.currentValue).toLocaleString()} / $${Math.floor(threshold).toLocaleString()} ${isSufficient ? "\u2728" : ""}</text>
|
||||
<text x="${f.x + f.segmentLength / 2}" y="${f.y - 2}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">$${Math.floor(f.data.currentValue).toLocaleString()} / $${Math.floor(threshold).toLocaleString()} ${isSufficient ? "✨" : ""}</text>
|
||||
<rect x="${f.x}" y="${f.y + f.riverWidth + 4}" width="${f.segmentLength}" height="3" rx="1.5" fill="#334155"/>
|
||||
<rect x="${f.x}" y="${f.y + f.riverWidth + 4}" width="${f.segmentLength * fillRatio}" height="3" rx="1.5" fill="${colors[0]}"/>`;
|
||||
}
|
||||
|
|
@ -471,7 +471,7 @@ class FolkBudgetRiver extends HTMLElement {
|
|||
${renderSufficiencyBadge(score, layout.width - 70, 10)}
|
||||
</svg>
|
||||
<div class="controls">
|
||||
<button class="${this.simulating ? "active" : ""}" data-action="toggle-sim">${this.simulating ? "\u23F8 Pause" : "\u25B6 Simulate"}</button>
|
||||
<button class="${this.simulating ? "active" : ""}" data-action="toggle-sim">${this.simulating ? "⏸ Pause" : "▶ Simulate"}</button>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="legend-item"><div class="legend-dot" style="background:#10b981"></div> Inflow</div>
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ routes.get("/flow/:flowId", (c) => {
|
|||
export const fundsModule: RSpaceModule = {
|
||||
id: "rfunds",
|
||||
name: "rFunds",
|
||||
icon: "\uD83C\uDF0A",
|
||||
icon: "🌊",
|
||||
description: "Budget flows, river visualization, and treasury management",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -546,7 +546,7 @@ routes.get("/", (c) => {
|
|||
export const inboxModule: RSpaceModule = {
|
||||
id: "rinbox",
|
||||
name: "rInbox",
|
||||
icon: "\u{1F4E8}",
|
||||
icon: "📨",
|
||||
description: "Collaborative email with multisig approval",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ class FolkMapViewer extends HTMLElement {
|
|||
|
||||
${this.rooms.length > 0 ? this.rooms.map((r) => `
|
||||
<div class="room-card" data-room="${r}">
|
||||
<span class="room-icon">\u{1F5FA}</span>
|
||||
<span class="room-icon">🗺</span>
|
||||
<span class="room-name">${this.esc(r)}</span>
|
||||
</div>
|
||||
`).join("") : ""}
|
||||
|
|
@ -181,14 +181,14 @@ class FolkMapViewer extends HTMLElement {
|
|||
const shareUrl = `${window.location.origin}/${this.space}/maps/${this.room}`;
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="lobby">\u2190 Rooms</button>
|
||||
<span class="rapp-nav__title">\u{1F5FA} ${this.esc(this.room)}</span>
|
||||
<button class="rapp-nav__back" data-back="lobby">← Rooms</button>
|
||||
<span class="rapp-nav__title">🗺 ${this.esc(this.room)}</span>
|
||||
<span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}"></span>
|
||||
</div>
|
||||
|
||||
<div class="map-container">
|
||||
<div class="map-placeholder">
|
||||
<p style="font-size:48px">\u{1F30D}</p>
|
||||
<p style="font-size:48px">🌍</p>
|
||||
<p style="font-size:16px">Map Room: <strong>${this.esc(this.room)}</strong></p>
|
||||
<p>Connect the MapLibre GL library to display the interactive map.</p>
|
||||
<p style="font-size:12px;color:#555">WebSocket sync: ${this.syncStatus}</p>
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ routes.get("/:room", (c) => {
|
|||
export const mapsModule: RSpaceModule = {
|
||||
id: "rmaps",
|
||||
name: "rMaps",
|
||||
icon: "\u{1F5FA}",
|
||||
icon: "🗺",
|
||||
description: "Real-time collaborative location sharing and indoor/outdoor maps",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
|
||||
<div class="graph-canvas">
|
||||
<div class="placeholder">
|
||||
<p style="font-size:48px">\u{1F578}\u{FE0F}</p>
|
||||
<p style="font-size:48px">🕸️</p>
|
||||
<p style="font-size:16px">Community Relationship Graph</p>
|
||||
<p>Connect the force-directed layout engine to visualize your network.</p>
|
||||
<p style="font-size:12px;color:#444">Automerge CRDT sync + d3-force layout</p>
|
||||
|
|
@ -131,7 +131,7 @@ class FolkGraphViewer extends HTMLElement {
|
|||
${this.workspaces.map(ws => `
|
||||
<div class="ws-card">
|
||||
<div class="ws-name">${this.esc(ws.name || ws.slug)}</div>
|
||||
<div class="ws-meta">${ws.nodeCount || 0} nodes \u00B7 ${ws.edgeCount || 0} edges</div>
|
||||
<div class="ws-meta">${ws.nodeCount || 0} nodes · ${ws.edgeCount || 0} edges</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ routes.get("/", (c) => {
|
|||
export const networkModule: RSpaceModule = {
|
||||
id: "rnetwork",
|
||||
name: "rNetwork",
|
||||
icon: "\u{1F310}",
|
||||
icon: "🌐",
|
||||
description: "Community relationship graph visualization with CRM sync",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -429,14 +429,14 @@ class FolkNotesApp extends HTMLElement {
|
|||
|
||||
private getNoteIcon(type: string): string {
|
||||
switch (type) {
|
||||
case "NOTE": return "\u{1F4DD}";
|
||||
case "CODE": return "\u{1F4BB}";
|
||||
case "BOOKMARK": return "\u{1F517}";
|
||||
case "IMAGE": return "\u{1F5BC}";
|
||||
case "AUDIO": return "\u{1F3A4}";
|
||||
case "FILE": return "\u{1F4CE}";
|
||||
case "CLIP": return "\u2702\uFE0F";
|
||||
default: return "\u{1F4C4}";
|
||||
case "NOTE": return "📝";
|
||||
case "CODE": return "💻";
|
||||
case "BOOKMARK": return "🔗";
|
||||
case "IMAGE": return "🖼";
|
||||
case "AUDIO": return "🎤";
|
||||
case "FILE": return "📎";
|
||||
case "CLIP": return "✂️";
|
||||
default: return "📄";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -573,7 +573,7 @@ class FolkNotesApp extends HTMLElement {
|
|||
: "";
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="notebooks">\u2190 Notebooks</button>
|
||||
<button class="rapp-nav__back" data-back="notebooks">← Notebooks</button>
|
||||
<span class="rapp-nav__title" style="color:${nb.cover_color}">${this.esc(nb.title)}${syncBadge}</span>
|
||||
<button class="rapp-nav__btn" id="create-note">+ New Note</button>
|
||||
</div>
|
||||
|
|
@ -606,7 +606,7 @@ class FolkNotesApp extends HTMLElement {
|
|||
const isAutomerge = !!(this.doc?.items?.[n.id]);
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="${this.selectedNotebook ? "notebook" : "notebooks"}">\u2190 ${this.selectedNotebook ? this.esc(this.selectedNotebook.title) : "Notebooks"}</button>
|
||||
<button class="rapp-nav__back" data-back="${this.selectedNotebook ? "notebook" : "notebooks"}">← ${this.selectedNotebook ? this.esc(this.selectedNotebook.title) : "Notebooks"}</button>
|
||||
${isAutomerge
|
||||
? `<input class="editable-title" id="note-title-input" value="${this.esc(n.title)}" placeholder="Note title...">`
|
||||
: `<span class="rapp-nav__title">${this.getNoteIcon(n.type)} ${this.esc(n.title)}</span>`
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ routes.get("/", (c) => {
|
|||
export const notesModule: RSpaceModule = {
|
||||
id: "rnotes",
|
||||
name: "rNotes",
|
||||
icon: "\u{1F4DD}",
|
||||
icon: "📝",
|
||||
description: "Notebooks with rich-text notes, voice transcription, and collaboration",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ class FolkPhotoGallery extends HTMLElement {
|
|||
const album = this.selectedAlbum!;
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="gallery">\u2190 Photos</button>
|
||||
<button class="rapp-nav__back" data-back="gallery">← Photos</button>
|
||||
<span class="rapp-nav__title">${this.esc(album.albumName)}</span>
|
||||
<div class="rapp-nav__actions">
|
||||
<a class="rapp-nav__btn rapp-nav__btn--secondary" href="${this.getImmichUrl()}/albums/${album.id}" target="_blank" rel="noopener">
|
||||
|
|
@ -362,7 +362,7 @@ class FolkPhotoGallery extends HTMLElement {
|
|||
|
||||
return `
|
||||
<div class="lightbox" data-lightbox>
|
||||
<button class="lightbox-close" data-close-lightbox>\u2715</button>
|
||||
<button class="lightbox-close" data-close-lightbox>✕</button>
|
||||
<img src="${this.originalUrl(asset.id)}" alt="${this.esc(asset.originalFileName)}">
|
||||
<div class="lightbox-info">
|
||||
${asset.originalFileName}
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ routes.get("/", (c) => {
|
|||
export const socialsModule: RSpaceModule = {
|
||||
id: "rsocials",
|
||||
name: "rSocials",
|
||||
icon: "\u{1F4E2}",
|
||||
icon: "📢",
|
||||
description: "Federated social feed aggregator for communities",
|
||||
routes,
|
||||
standaloneDomain: "rsocials.online",
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@ class FolkSwagDesigner extends HTMLElement {
|
|||
|
||||
private render() {
|
||||
const products = [
|
||||
{ id: "sticker", name: "Sticker Sheet", icon: "\u{1F4CB}", desc: "A4 vinyl stickers" },
|
||||
{ id: "poster", name: "Poster (A3)", icon: "\u{1F5BC}", desc: "A3 art print" },
|
||||
{ id: "tee", name: "T-Shirt", icon: "\u{1F455}", desc: "12x16\" DTG print" },
|
||||
{ id: "sticker", name: "Sticker Sheet", icon: "📋", desc: "A4 vinyl stickers" },
|
||||
{ id: "poster", name: "Poster (A3)", icon: "🖼", desc: "A3 art print" },
|
||||
{ id: "tee", name: "T-Shirt", icon: "👕", desc: "12x16\" DTG print" },
|
||||
];
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
|
|
@ -117,33 +117,33 @@ class FolkSwagDesigner extends HTMLElement {
|
|||
<div class="upload-area ${this.imagePreview ? 'has-image' : ''}">
|
||||
${this.imagePreview
|
||||
? `<img class="preview-img" src="${this.imagePreview}" alt="Preview">`
|
||||
: `<div class="upload-label">\u{1F4C1} Click or drag to upload artwork (PNG, JPG, SVG)</div>`}
|
||||
: `<div class="upload-label">📁 Click or drag to upload artwork (PNG, JPG, SVG)</div>`}
|
||||
<input type="file" accept="image/*">
|
||||
</div>
|
||||
|
||||
<input class="title-input" type="text" placeholder="Design title" value="${this.esc(this.designTitle)}">
|
||||
|
||||
<button class="generate-btn" ${!this.imageFile || this.generating ? 'disabled' : ''}>
|
||||
${this.generating ? '\u23F3 Generating...' : '\u{1F680} Generate Print-Ready Files'}
|
||||
${this.generating ? '⏳ Generating...' : '🚀 Generate Print-Ready Files'}
|
||||
</button>
|
||||
|
||||
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
|
||||
|
||||
${this.artifact ? `
|
||||
<div class="result">
|
||||
<h3 class="result-title">\u2705 ${this.esc(this.artifact.payload?.title || "Artifact")}</h3>
|
||||
<h3 class="result-title">✅ ${this.esc(this.artifact.payload?.title || "Artifact")}</h3>
|
||||
<div class="result-meta">
|
||||
${this.esc(this.artifact.spec?.product_type || "")} \u2022
|
||||
${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm \u2022
|
||||
${this.esc(this.artifact.spec?.product_type || "")} •
|
||||
${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm •
|
||||
${this.artifact.spec?.dpi}dpi
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
${Object.entries(this.artifact.render_targets || {}).map(([key, target]: [string, any]) => `
|
||||
<a class="result-btn result-btn-primary" href="${target.url}" target="_blank">\u{2B07} Download ${target.format.toUpperCase()}</a>
|
||||
<a class="result-btn result-btn-primary" href="${target.url}" target="_blank">⬇ Download ${target.format.toUpperCase()}</a>
|
||||
`).join("")}
|
||||
<button class="result-btn result-btn-secondary" data-action="copy-json">\u{1F4CB} Copy Artifact JSON</button>
|
||||
<button class="result-btn result-btn-secondary" data-action="copy-json">📋 Copy Artifact JSON</button>
|
||||
</div>
|
||||
<span class="json-toggle">Show artifact envelope \u25BC</span>
|
||||
<span class="json-toggle">Show artifact envelope ▼</span>
|
||||
<pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
|
||||
</div>` : ""}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ routes.get("/", (c) => {
|
|||
export const swagModule: RSpaceModule = {
|
||||
id: "rswag",
|
||||
name: "rSwag",
|
||||
icon: "\u{1F3A8}",
|
||||
icon: "🎨",
|
||||
description: "Design print-ready swag: stickers, posters, tees",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ class FolkRoutePlanner extends HTMLElement {
|
|||
// Compute crossing angle
|
||||
const dot = Math.abs(tA.dx * tB.dx + tA.dy * tB.dy);
|
||||
const angle = Math.acos(Math.min(1, dot)) * (180 / Math.PI);
|
||||
svg += `<text x="${(pt.x + 14).toFixed(1)}" y="${(pt.y + 18).toFixed(1)}" fill="rgba(255,255,255,0.6)" font-size="8" font-family="monospace">${angle.toFixed(1)}\u00B0</text>`;
|
||||
svg += `<text x="${(pt.x + 14).toFixed(1)}" y="${(pt.y + 18).toFixed(1)}" fill="rgba(255,255,255,0.6)" font-size="8" font-family="monospace">${angle.toFixed(1)}°</text>`;
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
|
@ -494,8 +494,8 @@ class FolkRoutePlanner extends HTMLElement {
|
|||
<div class="svg-container">${this.renderSVG()}</div>
|
||||
${this.renderInfo()}
|
||||
<div class="math-note">
|
||||
Conic: Ax\u00B2+Bxy+Cy\u00B2+Dx+Ey+F=0 | Discriminant \u0394=B\u00B2\u22124AC |
|
||||
Intersection via Sylvester resultant \u2192 degree-4 polynomial \u2192 companion matrix eigenvalues
|
||||
Conic: Ax²+Bxy+Cy²+Dx+Ey+F=0 | Discriminant Δ=B²−4AC |
|
||||
Intersection via Sylvester resultant → degree-4 polynomial → companion matrix eigenvalues
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,10 +124,10 @@ class FolkTripsPlanner extends HTMLElement {
|
|||
<div class="trip-card" data-trip="${t.id}">
|
||||
<div class="trip-name">${this.esc(t.title)}</div>
|
||||
<div class="trip-meta">
|
||||
${t.destination_count || 0} destinations \u00B7
|
||||
${t.destination_count || 0} destinations ·
|
||||
${t.start_date ? new Date(t.start_date).toLocaleDateString() : "No dates"}
|
||||
</div>
|
||||
${t.budget_total ? `<div class="trip-meta">Budget: $${parseFloat(t.budget_total).toFixed(0)} \u00B7 Spent: $${parseFloat(t.total_spent || 0).toFixed(0)}</div>` : ""}
|
||||
${t.budget_total ? `<div class="trip-meta">Budget: $${parseFloat(t.budget_total).toFixed(0)} · Spent: $${parseFloat(t.total_spent || 0).toFixed(0)}</div>` : ""}
|
||||
<span class="trip-status status-${t.status || "PLANNING"}">${t.status || "PLANNING"}</span>
|
||||
</div>
|
||||
`).join("")}
|
||||
|
|
@ -143,7 +143,7 @@ class FolkTripsPlanner extends HTMLElement {
|
|||
const t = this.trip;
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="list">\u2190 Trips</button>
|
||||
<button class="rapp-nav__back" data-back="list">← Trips</button>
|
||||
<span class="rapp-nav__title">${this.esc(t.title)}</span>
|
||||
<span class="trip-status status-${t.status || "PLANNING"}">${t.status || "PLANNING"}</span>
|
||||
</div>
|
||||
|
|
@ -165,7 +165,7 @@ class FolkTripsPlanner extends HTMLElement {
|
|||
return `
|
||||
<div class="section-title">Trip Details</div>
|
||||
<div class="item-row"><span class="item-title">${t.description || "No description"}</span></div>
|
||||
${t.start_date ? `<div class="item-row"><span class="item-title">Dates: ${new Date(t.start_date).toLocaleDateString()} \u2014 ${t.end_date ? new Date(t.end_date).toLocaleDateString() : "open"}</span></div>` : ""}
|
||||
${t.start_date ? `<div class="item-row"><span class="item-title">Dates: ${new Date(t.start_date).toLocaleDateString()} — ${t.end_date ? new Date(t.end_date).toLocaleDateString() : "open"}</span></div>` : ""}
|
||||
${t.budget_total ? `
|
||||
<div class="section-title">Budget</div>
|
||||
<div class="item-row" style="flex-direction:column;align-items:stretch">
|
||||
|
|
@ -177,17 +177,17 @@ class FolkTripsPlanner extends HTMLElement {
|
|||
</div>
|
||||
` : ""}
|
||||
<div class="section-title">Summary</div>
|
||||
<div class="item-row"><span class="item-meta">${(t.destinations || []).length} destinations \u00B7 ${(t.itinerary || []).length} activities \u00B7 ${(t.bookings || []).length} bookings \u00B7 ${(t.packing || []).length} packing items</span></div>
|
||||
<div class="item-row"><span class="item-meta">${(t.destinations || []).length} destinations · ${(t.itinerary || []).length} activities · ${(t.bookings || []).length} bookings · ${(t.packing || []).length} packing items</span></div>
|
||||
`;
|
||||
}
|
||||
case "destinations":
|
||||
return (t.destinations || []).length > 0
|
||||
? (t.destinations || []).map((d: any) => `
|
||||
<div class="item-row">
|
||||
<span style="font-size:20px">\u{1F4CD}</span>
|
||||
<span style="font-size:20px">📍</span>
|
||||
<div style="flex:1">
|
||||
<div class="item-title">${this.esc(d.name)}</div>
|
||||
<div class="item-meta">${d.country || ""} ${d.arrival_date ? `\u00B7 ${new Date(d.arrival_date).toLocaleDateString()}` : ""}</div>
|
||||
<div class="item-meta">${d.country || ""} ${d.arrival_date ? `· ${new Date(d.arrival_date).toLocaleDateString()}` : ""}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")
|
||||
|
|
@ -211,7 +211,7 @@ class FolkTripsPlanner extends HTMLElement {
|
|||
<span class="badge">${b.type || "OTHER"}</span>
|
||||
<div style="flex:1">
|
||||
<div class="item-title">${this.esc(b.provider || "Booking")}</div>
|
||||
<div class="item-meta">${b.confirmation_number ? `#${b.confirmation_number}` : ""} ${b.cost ? `\u00B7 $${parseFloat(b.cost).toFixed(0)}` : ""}</div>
|
||||
<div class="item-meta">${b.confirmation_number ? `#${b.confirmation_number}` : ""} ${b.cost ? `· $${parseFloat(b.cost).toFixed(0)}` : ""}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ routes.get("/", (c) => {
|
|||
export const tripsModule: RSpaceModule = {
|
||||
id: "rtrips",
|
||||
name: "rTrips",
|
||||
icon: "\u{2708}\u{FE0F}",
|
||||
icon: "✈️",
|
||||
description: "Collaborative trip planner with itinerary, bookings, and expense splitting",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ routes.get("/", (c) => {
|
|||
export const tubeModule: RSpaceModule = {
|
||||
id: "rtube",
|
||||
name: "rTube",
|
||||
icon: "\u{1F3AC}",
|
||||
icon: "🎬",
|
||||
description: "Community video hosting & live streaming",
|
||||
routes,
|
||||
landingPage: renderLanding,
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ class FolkVoteDashboard extends HTMLElement {
|
|||
const s = this.selectedSpace!;
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="spaces">\u2190 Spaces</button>
|
||||
<button class="rapp-nav__back" data-back="spaces">← Spaces</button>
|
||||
<span class="rapp-nav__title">${this.esc(s.name)} — Proposals</span>
|
||||
</div>
|
||||
${this.proposals.length === 0 ? '<div class="empty">No proposals yet.</div>' : ""}
|
||||
|
|
@ -264,7 +264,7 @@ class FolkVoteDashboard extends HTMLElement {
|
|||
const p = this.selectedProposal!;
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="proposals">\u2190 Proposals</button>
|
||||
<button class="rapp-nav__back" data-back="proposals">← Proposals</button>
|
||||
<span class="rapp-nav__title">${this.esc(p.title)}</span>
|
||||
<span class="badge" style="background:${this.getStatusColor(p.status)}20;color:${this.getStatusColor(p.status)};margin-left:8px">${p.status}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ routes.get("/", (c) => {
|
|||
export const voteModule: RSpaceModule = {
|
||||
id: "rvote",
|
||||
name: "rVote",
|
||||
icon: "\u{1F5F3}",
|
||||
icon: "🗳",
|
||||
description: "Conviction voting engine for collaborative governance",
|
||||
routes,
|
||||
standaloneDomain: "rvote.online",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ routes.get("/", (c) => {
|
|||
export const walletModule: RSpaceModule = {
|
||||
id: "rwallet",
|
||||
name: "rWallet",
|
||||
icon: "\uD83D\uDCB0",
|
||||
icon: "💰",
|
||||
description: "Multichain Safe wallet visualization and treasury management",
|
||||
routes,
|
||||
standaloneDomain: "rwallet.online",
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ class FolkWorkBoard extends HTMLElement {
|
|||
${this.workspaces.length > 0 ? `<div class="workspace-grid">
|
||||
${this.workspaces.map(ws => `
|
||||
<div class="workspace-card" data-ws="${ws.slug}">
|
||||
<div class="ws-name">${this.esc(ws.icon || "\u{1F4CB}")} ${this.esc(ws.name)}</div>
|
||||
<div class="ws-meta">${ws.task_count || 0} tasks \u00B7 ${ws.member_count || 0} members</div>
|
||||
<div class="ws-name">${this.esc(ws.icon || "📋")} ${this.esc(ws.name)}</div>
|
||||
<div class="ws-meta">${ws.task_count || 0} tasks · ${ws.member_count || 0} members</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>` : `<div class="empty">
|
||||
|
|
@ -181,7 +181,7 @@ class FolkWorkBoard extends HTMLElement {
|
|||
private renderBoard(): string {
|
||||
return `
|
||||
<div class="rapp-nav">
|
||||
<button class="rapp-nav__back" data-back="list">\u2190 Workspaces</button>
|
||||
<button class="rapp-nav__back" data-back="list">← Workspaces</button>
|
||||
<span class="rapp-nav__title">${this.esc(this.workspaceSlug)}</span>
|
||||
<button class="rapp-nav__btn" id="create-task">+ New Task</button>
|
||||
</div>
|
||||
|
|
@ -213,7 +213,7 @@ class FolkWorkBoard extends HTMLElement {
|
|||
${(task.labels || []).map((l: string) => `<span class="badge">${this.esc(l)}</span>`).join("")}
|
||||
</div>
|
||||
<div class="move-btns">
|
||||
${otherStatuses.map(s => `<button class="move-btn" data-move="${task.id}" data-to="${s}">\u2192 ${this.esc(s.replace(/_/g, " ").substring(0, 8))}</button>`).join("")}
|
||||
${otherStatuses.map(s => `<button class="move-btn" data-move="${task.id}" data-to="${s}">→ ${this.esc(s.replace(/_/g, " ").substring(0, 8))}</button>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ routes.get("/", (c) => {
|
|||
export const workModule: RSpaceModule = {
|
||||
id: "rwork",
|
||||
name: "rWork",
|
||||
icon: "\u{1F4CB}",
|
||||
icon: "📋",
|
||||
description: "Kanban workspace boards for collaborative task management",
|
||||
routes,
|
||||
standaloneDomain: "rwork.online",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "thread",
|
||||
stepNumber: 1,
|
||||
content:
|
||||
"Something is growing in the mycelium... \ud83c\udf44\n\nFor the past 2 years, we've been building the infrastructure for a regenerative economy.\n\nOn Feb 24, we reveal everything.\n\nA thread on why the old financial system is composting itself \ud83e\uddf5\ud83d\udc47",
|
||||
"Something is growing in the mycelium... 🍄\n\nFor the past 2 years, we've been building the infrastructure for a regenerative economy.\n\nOn Feb 24, we reveal everything.\n\nA thread on why the old financial system is composting itself 🧵👇",
|
||||
mediaUrl: "",
|
||||
mediaType: "",
|
||||
scheduledAt: "2026-02-21T09:00:00",
|
||||
|
|
@ -93,7 +93,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "article",
|
||||
stepNumber: 2,
|
||||
content:
|
||||
"The regenerative finance movement isn't just about returns \u2014 it's about redesigning incentive structures from the ground up.\n\nIn this article, I break down why mycelial network theory offers the best model for decentralized economic coordination.\n\n3 key insights from 2 years of building MycoFi Earth...",
|
||||
"The regenerative finance movement isn't just about returns — it's about redesigning incentive structures from the ground up.\n\nIn this article, I break down why mycelial network theory offers the best model for decentralized economic coordination.\n\n3 key insights from 2 years of building MycoFi Earth...",
|
||||
mediaUrl: "",
|
||||
mediaType: "image",
|
||||
scheduledAt: "2026-02-22T11:00:00",
|
||||
|
|
@ -113,7 +113,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "carousel",
|
||||
stepNumber: 3,
|
||||
content:
|
||||
"5 Ways Mycelium Networks Mirror the Future of Finance \ud83c\udf0d\ud83c\udf44\n\nSlide 1: The problem with extractive finance\nSlide 2: How mycelium redistributes nutrients\nSlide 3: Token-weighted funding circles\nSlide 4: Community governance that actually works\nSlide 5: Join the launch \u2014 Feb 24",
|
||||
"5 Ways Mycelium Networks Mirror the Future of Finance 🌍🍄\n\nSlide 1: The problem with extractive finance\nSlide 2: How mycelium redistributes nutrients\nSlide 3: Token-weighted funding circles\nSlide 4: Community governance that actually works\nSlide 5: Join the launch — Feb 24",
|
||||
mediaUrl: "",
|
||||
mediaType: "carousel",
|
||||
scheduledAt: "2026-02-23T14:00:00",
|
||||
|
|
@ -141,7 +141,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "video",
|
||||
stepNumber: 4,
|
||||
content:
|
||||
"MycoFi Earth \u2014 Official Launch Video\n\nThe regenerative economy starts here. Watch how mycelial intelligence is reshaping finance, governance, and community coordination.\n\nFeaturing interviews with 12 builders from the ecosystem.\n\n[18:42]",
|
||||
"MycoFi Earth — Official Launch Video\n\nThe regenerative economy starts here. Watch how mycelial intelligence is reshaping finance, governance, and community coordination.\n\nFeaturing interviews with 12 builders from the ecosystem.\n\n[18:42]",
|
||||
mediaUrl: "",
|
||||
mediaType: "video",
|
||||
scheduledAt: "2026-02-24T10:00:00",
|
||||
|
|
@ -166,7 +166,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "thread",
|
||||
stepNumber: 5,
|
||||
content:
|
||||
"\ud83c\udf44 MycoFi Earth is LIVE \ud83c\udf44\n\nAfter 2 years of building, the regenerative finance platform is here.\n\nWhat is it?\n\u2022 Token-weighted funding circles\n\u2022 Mycelial governance (no whales)\n\u2022 Composting mechanism for failed proposals\n\u2022 100% on-chain, 100% community-owned\n\n\ud83d\udc47 Full breakdown thread",
|
||||
"🍄 MycoFi Earth is LIVE 🍄\n\nAfter 2 years of building, the regenerative finance platform is here.\n\nWhat is it?\n• Token-weighted funding circles\n• Mycelial governance (no whales)\n• Composting mechanism for failed proposals\n• 100% on-chain, 100% community-owned\n\n👇 Full breakdown thread",
|
||||
mediaUrl: "",
|
||||
mediaType: "image",
|
||||
scheduledAt: "2026-02-24T10:15:00",
|
||||
|
|
@ -192,7 +192,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "text",
|
||||
stepNumber: 6,
|
||||
content:
|
||||
"Today we're launching MycoFi Earth \u2014 a regenerative finance platform modeled on mycelial networks.\n\nWhy it matters for the future of organizational design:\n\n1. Composting mechanism: Failed proposals return resources to the network\n2. Nutrient routing: Funds flow to where they're needed most\n3. No single point of failure: True decentralization\n\nFull video + docs in comments \u2193",
|
||||
"Today we're launching MycoFi Earth — a regenerative finance platform modeled on mycelial networks.\n\nWhy it matters for the future of organizational design:\n\n1. Composting mechanism: Failed proposals return resources to the network\n2. Nutrient routing: Funds flow to where they're needed most\n3. No single point of failure: True decentralization\n\nFull video + docs in comments ↓",
|
||||
mediaUrl: "",
|
||||
mediaType: "image",
|
||||
scheduledAt: "2026-02-24T11:00:00",
|
||||
|
|
@ -220,7 +220,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "reel",
|
||||
stepNumber: 7,
|
||||
content:
|
||||
"60-second explainer: How MycoFi Earth works \ud83c\udf44\u2728\n\nVisual breakdown of the token flow from contributor \u2192 funding circle \u2192 project \u2192 compost.\n\nSet to lo-fi beats with mycelium time-lapse footage.\n\nCTA: Link in bio to join the first funding circle.",
|
||||
"60-second explainer: How MycoFi Earth works 🍄✨\n\nVisual breakdown of the token flow from contributor → funding circle → project → compost.\n\nSet to lo-fi beats with mycelium time-lapse footage.\n\nCTA: Link in bio to join the first funding circle.",
|
||||
mediaUrl: "",
|
||||
mediaType: "video",
|
||||
scheduledAt: "2026-02-25T12:00:00",
|
||||
|
|
@ -246,7 +246,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "text",
|
||||
stepNumber: 8,
|
||||
content:
|
||||
"We just launched MycoFi Earth and the response has been incredible \ud83c\udf1f\n\nThe idea is simple: what if finance worked like mycelium?\n\nMycelium doesn't hoard \u2014 it redistributes. MycoFi applies that logic to community funding.\n\nEarly access is open. Come grow with us \ud83c\udf31",
|
||||
"We just launched MycoFi Earth and the response has been incredible 🌟\n\nThe idea is simple: what if finance worked like mycelium?\n\nMycelium doesn't hoard — it redistributes. MycoFi applies that logic to community funding.\n\nEarly access is open. Come grow with us 🌱",
|
||||
mediaUrl: "",
|
||||
mediaType: "",
|
||||
scheduledAt: "2026-02-25T14:00:00",
|
||||
|
|
@ -266,7 +266,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
postType: "text",
|
||||
stepNumber: 9,
|
||||
content:
|
||||
"MycoFi Earth just went live \ud83c\udf44\n\nIt's a regenerative finance platform where funding flows like nutrients through a mycelial network.\n\nNo VCs. No whales. Just communities funding what matters.\n\nmycofi.earth",
|
||||
"MycoFi Earth just went live 🍄\n\nIt's a regenerative finance platform where funding flows like nutrients through a mycelial network.\n\nNo VCs. No whales. Just communities funding what matters.\n\nmycofi.earth",
|
||||
mediaUrl: "",
|
||||
mediaType: "",
|
||||
scheduledAt: "2026-02-25T15:00:00",
|
||||
|
|
@ -286,7 +286,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
height: 160,
|
||||
rotation: 0,
|
||||
content:
|
||||
"# \ud83c\udf44 MycoFi Earth Launch Campaign\n\n**Duration:** Feb 21\u201325, 2026 (5 days)\n**Platforms:** X, LinkedIn, Instagram, YouTube, Threads, Bluesky\n**Posts:** 9 total across 3 phases\n\n| Phase | Days | Posts |\n|-------|------|-------|\n| Pre-Launch Hype | Day -3 to -1 | 3 posts |\n| Launch Day | Day 0 | 3 posts |\n| Amplification | Day +1 | 3 posts |",
|
||||
"# 🍄 MycoFi Earth Launch Campaign\n\n**Duration:** Feb 21–25, 2026 (5 days)\n**Platforms:** X, LinkedIn, Instagram, YouTube, Threads, Bluesky\n**Posts:** 9 total across 3 phases\n\n| Phase | Days | Posts |\n|-------|------|-------|\n| Pre-Launch Hype | Day -3 to -1 | 3 posts |\n| Launch Day | Day 0 | 3 posts |\n| Amplification | Day +1 | 3 posts |",
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
|
@ -301,7 +301,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
height: 36,
|
||||
rotation: 0,
|
||||
content:
|
||||
"### \ud83d\udce3 Phase 1: Pre-Launch Hype (Feb 21\u201323)",
|
||||
"### 📣 Phase 1: Pre-Launch Hype (Feb 21–23)",
|
||||
},
|
||||
{
|
||||
id: "label-phase2",
|
||||
|
|
@ -311,7 +311,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
width: COL_WIDTH * 3 + COL_GAP * 2,
|
||||
height: 36,
|
||||
rotation: 0,
|
||||
content: "### \ud83d\ude80 Phase 2: Launch Day (Feb 24)",
|
||||
content: "### 🚀 Phase 2: Launch Day (Feb 24)",
|
||||
},
|
||||
{
|
||||
id: "label-phase3",
|
||||
|
|
@ -322,7 +322,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
|||
height: 36,
|
||||
rotation: 0,
|
||||
content:
|
||||
"### \ud83d\udce1 Phase 3: Amplification (Feb 25)",
|
||||
"### 📡 Phase 3: Amplification (Feb 25)",
|
||||
},
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ const DEMO_SHAPES: Record<string, unknown>[] = [
|
|||
totalSupply: 100,
|
||||
issuedSupply: 100,
|
||||
tokenColor: "#6d28d9",
|
||||
tokenIcon: "\uD83D\uDDF3\uFE0F",
|
||||
tokenIcon: "🗳️",
|
||||
createdBy: "Maya",
|
||||
createdAt: "2026-06-15T10:00:00.000Z",
|
||||
},
|
||||
|
|
@ -458,7 +458,7 @@ const DEMO_SHAPES: Record<string, unknown>[] = [
|
|||
totalSupply: 500,
|
||||
issuedSupply: 155,
|
||||
tokenColor: "#059669",
|
||||
tokenIcon: "\u2B50",
|
||||
tokenIcon: "⭐",
|
||||
createdBy: "Maya",
|
||||
createdAt: "2026-06-20T14:00:00.000Z",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -270,11 +270,11 @@ export class RStackIdentity extends HTMLElement {
|
|||
<div class="dropdown-header">${displayName}</div>
|
||||
${notifsHTML}
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item" data-action="add-email">\u2709\uFE0F Add Email</button>
|
||||
<button class="dropdown-item" data-action="add-device">\uD83D\uDCF1 Add Second Device</button>
|
||||
<button class="dropdown-item" data-action="add-recovery">\uD83D\uDEE1\uFE0F Add Social Recovery</button>
|
||||
<button class="dropdown-item" data-action="add-email">✉️ Add Email</button>
|
||||
<button class="dropdown-item" data-action="add-device">📱 Add Second Device</button>
|
||||
<button class="dropdown-item" data-action="add-recovery">🛡️ Add Social Recovery</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item dropdown-item--danger" data-action="signout">\uD83D\uDEAA Sign Out</button>
|
||||
<button class="dropdown-item dropdown-item--danger" data-action="signout">🚪 Sign Out</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -447,7 +447,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
autoResolveSpace(data.token, data.username || "");
|
||||
} catch (err: any) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = "\uD83D\uDD11 Sign In with Passkey";
|
||||
btn.innerHTML = "🔑 Sign In with Passkey";
|
||||
errEl.textContent = err.name === "NotAllowedError" ? "Authentication was cancelled." : err.message || "Authentication failed.";
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -111,9 +111,9 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
|
||||
#visibilityInfo(s: SpaceInfo): { cls: string; label: string } {
|
||||
const v = s.visibility || "public_read";
|
||||
if (v === "members_only") return { cls: "vis-private", label: "\uD83D\uDD12" };
|
||||
if (v === "authenticated") return { cls: "vis-permissioned", label: "\uD83D\uDD11" };
|
||||
return { cls: "vis-public", label: "\uD83D\uDD13" };
|
||||
if (v === "members_only") return { cls: "vis-private", label: "🔒" };
|
||||
if (v === "authenticated") return { cls: "vis-permissioned", label: "🔑" };
|
||||
return { cls: "vis-public", label: "🔓" };
|
||||
}
|
||||
|
||||
#renderMenu(menu: HTMLElement, current: string) {
|
||||
|
|
@ -146,7 +146,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
return `
|
||||
<a class="item ${vis.cls} ${s.slug === current ? "active" : ""}"
|
||||
href="${rspaceNavUrl(s.slug, moduleId)}">
|
||||
<span class="item-icon">${s.icon || "\uD83C\uDF10"}</span>
|
||||
<span class="item-icon">${s.icon || "🌐"}</span>
|
||||
<span class="item-name">${s.name}</span>
|
||||
<span class="item-vis ${vis.cls}">${vis.label}</span>
|
||||
</a>`;
|
||||
|
|
@ -164,7 +164,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
return `
|
||||
<a class="item ${vis.cls} ${s.slug === current ? "active" : ""}"
|
||||
href="${rspaceNavUrl(s.slug, moduleId)}">
|
||||
<span class="item-icon">${s.icon || "\uD83C\uDF10"}</span>
|
||||
<span class="item-icon">${s.icon || "🌐"}</span>
|
||||
<span class="item-name">${s.name}</span>
|
||||
<span class="item-vis ${vis.cls}">${vis.label}</span>
|
||||
</a>`;
|
||||
|
|
@ -182,7 +182,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
const pending = s.pendingRequest;
|
||||
return `
|
||||
<div class="item item--discover ${vis.cls}">
|
||||
<span class="item-icon">${s.icon || "\uD83C\uDF10"}</span>
|
||||
<span class="item-icon">${s.icon || "🌐"}</span>
|
||||
<span class="item-name">${s.name}</span>
|
||||
<span class="item-vis ${vis.cls}">${vis.label}</span>
|
||||
${pending
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
const contained = containedSet.has(f.id);
|
||||
return `<span class="io-chip io-chip--out ${contained ? "io-chip--contained" : ""}"
|
||||
style="--chip-color:${FLOW_COLORS[f.kind]}" title="${f.description || f.name}">
|
||||
<span class="io-dot"></span>${f.name}${contained ? '<span class="io-lock">\uD83D\uDD12</span>' : ""}
|
||||
<span class="io-dot"></span>${f.name}${contained ? '<span class="io-lock">🔒</span>' : ""}
|
||||
</span>`;
|
||||
}).join("");
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
const scrubberHtml = `
|
||||
<div class="time-scrubber">
|
||||
<button class="scrubber-playpause" id="scrubber-playpause" title="${this.#simPlaying ? "Pause" : "Play"}">
|
||||
${this.#simPlaying ? "\u23F8" : "\u25B6"}
|
||||
${this.#simPlaying ? "⏸" : "▶"}
|
||||
</button>
|
||||
<input type="range" class="scrubber-range" id="scrubber-range"
|
||||
min="0.1" max="5" step="0.1" value="${this.#simSpeed}" />
|
||||
|
|
@ -451,7 +451,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
</div>
|
||||
<div class="stack-legend">${legendHtml}</div>
|
||||
${scrubberHtml}
|
||||
${this.#layers.length >= 2 ? `<div class="stack-hint">Drag between layers to create a flow \u00b7 Drag empty space to orbit</div>` : ""}
|
||||
${this.#layers.length >= 2 ? `<div class="stack-hint">Drag between layers to create a flow · Drag empty space to orbit</div>` : ""}
|
||||
${this.#flowDialogOpen ? this.#renderFlowDialog() : ""}
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -486,7 +486,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
<div class="flow-dialog-header">New Flow</div>
|
||||
<div class="flow-dialog-route">
|
||||
<span class="flow-dialog-badge" style="background:${srcBadge?.color || "#94a3b8"}">${srcBadge?.badge || srcLayer.moduleId}</span>
|
||||
<span class="flow-dialog-arrow">\u2192</span>
|
||||
<span class="flow-dialog-arrow">→</span>
|
||||
<span class="flow-dialog-badge" style="background:${tgtBadge?.color || "#94a3b8"}">${tgtBadge?.badge || tgtLayer.moduleId}</span>
|
||||
</div>
|
||||
<div class="flow-dialog-field">
|
||||
|
|
@ -782,7 +782,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
|
||||
scrubberPlaypause?.addEventListener("click", () => {
|
||||
this.#simPlaying = !this.#simPlaying;
|
||||
scrubberPlaypause.textContent = this.#simPlaying ? "\u23F8" : "\u25B6";
|
||||
scrubberPlaypause.textContent = this.#simPlaying ? "⏸" : "▶";
|
||||
scrubberPlaypause.title = this.#simPlaying ? "Pause" : "Play";
|
||||
const state = this.#simPlaying ? "running" : "paused";
|
||||
this.#shadow.querySelectorAll<HTMLElement>(".flow-particle").forEach(p => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue