Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-02-27 16:49:00 -08:00
commit 1165a7fd5a
65 changed files with 346 additions and 286 deletions

View File

@ -0,0 +1,45 @@
---
id: TASK-70
title: Default selector tool with marquee multi-select
status: Done
assignee: []
created_date: '2026-02-28 00:36'
updated_date: '2026-02-28 00:36'
labels:
- canvas
- UX
- selection
dependencies: []
references:
- lib/folk-shape.ts
- website/canvas.html
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Replace single-click-to-pan with selector as the default canvas tool. Left-click-drag on background draws a blue marquee rectangle to select multiple shapes. Shift/Ctrl+click toggles additive selection. Panning moves to Space+drag, middle-click, or wheel/trackpad. Delete/Backspace removes all selected shapes. folk-shape highlighted state shows blue selection outline.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Click canvas background → nothing selected, no pan
- [x] #2 Click-drag on background → blue marquee rectangle appears
- [x] #3 Release marquee → all overlapping shapes highlighted with blue outline
- [x] #4 Shift+click a shape → adds/removes from selection
- [x] #5 Click a single shape → only that shape selected
- [x] #6 Hold Space + drag → pans the canvas (cursor shows grab)
- [x] #7 Middle-click + drag → pans the canvas
- [x] #8 Two-finger scroll → still pans (unchanged)
- [x] #9 Ctrl+scroll → still zooms (unchanged)
- [x] #10 Delete/Backspace → removes all selected shapes
- [x] #11 Click toolbar tool → crosshair cursor → click to place (unchanged)
- [x] #12 bunx tsc --noEmit passes
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented default selector tool with marquee multi-select. Changed folk-shape.ts highlighted CSS to show blue outline. Rewrote canvas.html pointer handlers: left-click-drag draws marquee selection rectangle, Space+drag and middle-click for panning, Shift/Ctrl+click for additive selection, Delete/Backspace to remove selected shapes. Commit 1d8fc2b on dev, merged to main.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@ -411,7 +411,7 @@ export class FolkBlender extends FolkShape {
if (this.#renderUrl) { if (this.#renderUrl) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#renderUrl)}" alt="3D Render" />`; this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#renderUrl)}" alt="3D Render" />`;
} else { } 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>';
} }
} }

View File

@ -150,15 +150,15 @@ const styles = css`
`; `;
const BOOKING_TYPE_ICONS: Record<string, string> = { const BOOKING_TYPE_ICONS: Record<string, string> = {
FLIGHT: "\u2708\uFE0F", FLIGHT: "✈️",
HOTEL: "\uD83C\uDFE8", HOTEL: "🏨",
CAR_RENTAL: "\uD83D\uDE97", CAR_RENTAL: "🚗",
TRAIN: "\uD83D\uDE84", TRAIN: "🚄",
BUS: "\uD83D\uDE8C", BUS: "🚌",
FERRY: "\u26F4\uFE0F", FERRY: "⛴️",
ACTIVITY: "\uD83C\uDFAF", ACTIVITY: "🎯",
RESTAURANT: "\uD83C\uDF7D\uFE0F", RESTAURANT: "🍽️",
OTHER: "\uD83D\uDCCC", OTHER: "📌",
}; };
declare global { declare global {
@ -253,11 +253,11 @@ export class FolkBooking extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header OTHER"> <div class="header OTHER">
<span class="header-title"> <span class="header-title">
<span class="type-icon">\uD83D\uDCCC</span> <span class="type-icon">📌</span>
<span class="type-label">Booking</span> <span class="type-label">Booking</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="booking-body"></div> <div class="booking-body"></div>

View File

@ -243,11 +243,11 @@ export class FolkBudget extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\uD83D\uDCB0</span> <span>💰</span>
<span>Budget</span> <span>Budget</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="budget-body"> <div class="budget-body">

View File

@ -242,18 +242,18 @@ export class FolkCalendar extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\u{1F4C5}</span> <span>📅</span>
<span>Calendar</span> <span>Calendar</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="calendar-container"> <div class="calendar-container">
<div class="calendar-nav"> <div class="calendar-nav">
<button class="prev-btn">\u276E</button> <button class="prev-btn"></button>
<span class="month-year"></span> <span class="month-year"></span>
<button class="next-btn">\u276F</button> <button class="next-btn"></button>
</div> </div>
<div class="weekdays"> <div class="weekdays">
<span class="weekday">Sun</span> <span class="weekday">Sun</span>

View File

@ -277,15 +277,15 @@ export class FolkCanvas extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header" data-drag> <div class="header" data-drag>
<div class="header-left"> <div class="header-left">
<span class="icon">\u{1F5BC}</span> <span class="icon">🖼</span>
<span class="source-name"></span> <span class="source-name"></span>
</div> </div>
<div class="header-right"> <div class="header-right">
<span class="permission-badge badge"></span> <span class="permission-badge badge"></span>
<div class="header-actions"> <div class="header-actions">
<button class="collapse-btn" title="Toggle collapse">\u25BC</button> <button class="collapse-btn" title="Toggle collapse"></button>
<button class="enter-space-btn" title="Open space">\u2197</button> <button class="enter-space-btn" title="Open space"></button>
<button class="close-btn" title="Remove">\u00D7</button> <button class="close-btn" title="Remove">×</button>
</div> </div>
</div> </div>
</div> </div>
@ -331,7 +331,7 @@ export class FolkCanvas extends FolkShape {
collapseBtn.addEventListener("click", (e) => { collapseBtn.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
this.collapsed = !this.#collapsed; this.collapsed = !this.#collapsed;
collapseBtn.textContent = this.#collapsed ? "\u25B6" : "\u25BC"; collapseBtn.textContent = this.#collapsed ? "▶" : "▼";
this.dispatchEvent(new CustomEvent("content-change", { detail: { collapsed: this.#collapsed } })); this.dispatchEvent(new CustomEvent("content-change", { detail: { collapsed: this.#collapsed } }));
}); });
@ -448,10 +448,10 @@ export class FolkCanvas extends FolkShape {
if (this.#collapsed) { if (this.#collapsed) {
content.innerHTML = ` content.innerHTML = `
<div class="collapsed-view"> <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-label">${this.#label || this.#sourceSlug}</div>
<div class="collapsed-meta">${this.#nestedShapes.size} shapes</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> </div>
`; `;
statusBar.style.display = "none"; statusBar.style.display = "none";

View File

@ -194,11 +194,11 @@ export class FolkDestination extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\uD83D\uDCCD</span> <span>📍</span>
<span>Destination</span> <span>Destination</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="dest-body"> <div class="dest-body">

View File

@ -239,11 +239,11 @@ export class FolkEmbed extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\u{1F517}</span> <span>🔗</span>
<span class="title-text">Embed</span> <span class="title-text">Embed</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
@ -347,7 +347,7 @@ export class FolkEmbed extends FolkShape {
urlInputContainer.innerHTML = ` urlInputContainer.innerHTML = `
<div class="unsupported"> <div class="unsupported">
<p>This content cannot be embedded in an iframe.</p> <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> </div>
`; `;
const openBtn = urlInputContainer.querySelector(".open-link"); const openBtn = urlInputContainer.querySelector(".open-link");

View File

@ -341,7 +341,7 @@ export class FolkFreeCAD extends FolkShape {
if (this.#previewUrl) { if (this.#previewUrl) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#previewUrl)}" alt="CAD Preview" />`; this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#previewUrl)}" alt="CAD Preview" />`;
} else { } 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>';
} }
} }

View File

@ -5,10 +5,10 @@ export type GoogleService = "gmail" | "drive" | "photos" | "calendar";
export type ItemVisibility = "local" | "shared"; export type ItemVisibility = "local" | "shared";
const SERVICE_ICONS: Record<GoogleService, string> = { const SERVICE_ICONS: Record<GoogleService, string> = {
gmail: "\u{1F4E7}", gmail: "📧",
drive: "\u{1F4C1}", drive: "📁",
photos: "\u{1F4F7}", photos: "📷",
calendar: "\u{1F4C5}", calendar: "📅",
}; };
const styles = css` const styles = css`
@ -213,7 +213,7 @@ export class FolkGoogleItem extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="card ${this.#visibility}" data-drag> <div class="card ${this.#visibility}" data-drag>
<span class="visibility-badge" title="Toggle visibility"> <span class="visibility-badge" title="Toggle visibility">
${this.#visibility === "local" ? "\u{1F512}" : "\u{1F310}"} ${this.#visibility === "local" ? "🔒" : "🌐"}
</span> </span>
<div class="header"> <div class="header">
<span class="service-icon">${SERVICE_ICONS[this.#service]}</span> <span class="service-icon">${SERVICE_ICONS[this.#service]}</span>
@ -245,7 +245,7 @@ export class FolkGoogleItem extends FolkShape {
e.stopPropagation(); e.stopPropagation();
this.#visibility = this.#visibility === "local" ? "shared" : "local"; 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.remove("local", "shared");
card.classList.add(this.#visibility); card.classList.add(this.#visibility);

View File

@ -364,7 +364,7 @@ export class FolkImageGen extends FolkShape {
if (!this.#imageArea) return; if (!this.#imageArea) return;
this.#imageArea.innerHTML = ` this.#imageArea.innerHTML = `
<div class="error">${this.#escapeHtml(this.#error || "Unknown error")}</div> <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) { if (this.#images.length === 0) {
this.#imageArea.innerHTML = ` this.#imageArea.innerHTML = `
<div class="placeholder"> <div class="placeholder">
<span class="placeholder-icon">\u{1F5BC}</span> <span class="placeholder-icon">🖼</span>
<span>Enter a prompt and click Generate</span> <span>Enter a prompt and click Generate</span>
</div> </div>
`; `;

View File

@ -156,13 +156,13 @@ export interface ItineraryEntry {
} }
const CATEGORY_ICONS: Record<string, string> = { const CATEGORY_ICONS: Record<string, string> = {
FLIGHT: "\u2708\uFE0F", FLIGHT: "✈️",
TRANSPORT: "\uD83D\uDE8C", TRANSPORT: "🚌",
ACCOMMODATION: "\uD83C\uDFE8", ACCOMMODATION: "🏨",
ACTIVITY: "\uD83C\uDFAF", ACTIVITY: "🎯",
MEAL: "\uD83C\uDF7D\uFE0F", MEAL: "🍽️",
FREE_TIME: "\u2600\uFE0F", FREE_TIME: "☀️",
OTHER: "\uD83D\uDCCC", OTHER: "📌",
}; };
declare global { declare global {
@ -222,11 +222,11 @@ export class FolkItinerary extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\uD83D\uDCC5</span> <span>📅</span>
<span class="title-text">Itinerary</span> <span class="title-text">Itinerary</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="itinerary-container"> <div class="itinerary-container">

View File

@ -434,14 +434,14 @@ export class FolkKiCAD extends FolkShape {
if (this.#schematicSvg) { if (this.#schematicSvg) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#schematicSvg)}" alt="Schematic" />`; this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#schematicSvg)}" alt="Schematic" />`;
} else { } 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; break;
case "board": case "board":
if (this.#boardSvg) { if (this.#boardSvg) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#boardSvg)}" alt="Board Layout" />`; this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#boardSvg)}" alt="Board Layout" />`;
} else { } 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; break;
case "drc": case "drc":
@ -451,14 +451,14 @@ export class FolkKiCAD extends FolkShape {
this.#previewArea.innerHTML = ` this.#previewArea.innerHTML = `
<div class="drc-results" style="padding:12px"> <div class="drc-results" style="padding:12px">
<h4 style="margin:0 0 8px">${passed <h4 style="margin:0 0 8px">${passed
? '<span class="pass">\u2705 DRC Passed</span>' ? '<span class="pass"> DRC Passed</span>'
: `<span class="fail">\u274C ${violations.length} Violation(s)</span>` : `<span class="fail"> ${violations.length} Violation(s)</span>`
}</h4> }</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> </div>
`; `;
} else { } 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; break;
} }

View File

@ -290,20 +290,20 @@ export class FolkMap extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\u{1F5FA}</span> <span>🗺</span>
<span>Map</span> <span>Map</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="map-container"> <div class="map-container">
<div class="loading">Loading map...</div> <div class="loading">Loading map...</div>
<div class="search-box"> <div class="search-box">
<input type="text" class="search-input" placeholder="Search location..." /> <input type="text" class="search-input" placeholder="Search location..." />
<button class="search-btn">\u{1F50D}</button> <button class="search-btn">🔍</button>
</div> </div>
<button class="locate-btn" title="My Location">\u{1F4CD}</button> <button class="locate-btn" title="My Location">📍</button>
<div class="map"></div> <div class="map"></div>
</div> </div>
`; `;

View File

@ -325,12 +325,12 @@ export class FolkObsNote extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\u{1F4DD}</span> <span>📝</span>
<input type="text" class="note-title" placeholder="Note title..." /> <input type="text" class="note-title" placeholder="Note title..." />
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="save-btn" title="Save">\u{1F4BE}</button> <button class="save-btn" title="Save">💾</button>
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="content"> <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="italic" title="Italic">I</button>
<button class="toolbar-btn" data-action="code" title="Code">&lt;/&gt;</button> <button class="toolbar-btn" data-action="code" title="Code">&lt;/&gt;</button>
<span class="toolbar-divider"></span> <span class="toolbar-divider"></span>
<button class="toolbar-btn" data-action="link" title="Link">\u{1F517}</button> <button class="toolbar-btn" data-action="link" title="Link">🔗</button>
<button class="toolbar-btn" data-action="list" title="List">\u2022</button> <button class="toolbar-btn" data-action="list" title="List"></button>
<button class="toolbar-btn" data-action="quote" title="Quote">"</button> <button class="toolbar-btn" data-action="quote" title="Quote">"</button>
<div class="mode-toggle"> <div class="mode-toggle">
<button class="mode-btn active" data-mode="edit">Edit</button> <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> <span class="chars">0 characters</span>
</div> </div>
<div class="save-status saved"> <div class="save-status saved">
<span>\u2713</span> <span></span>
<span>Saved</span> <span>Saved</span>
</div> </div>
</div> </div>
@ -609,10 +609,10 @@ export class FolkObsNote extends FolkShape {
if (this.#isDirty) { if (this.#isDirty) {
this.#saveStatusEl.className = "save-status unsaved"; this.#saveStatusEl.className = "save-status unsaved";
this.#saveStatusEl.innerHTML = "<span>\u2022</span><span>Unsaved</span>"; this.#saveStatusEl.innerHTML = "<span></span><span>Unsaved</span>";
} else { } else {
this.#saveStatusEl.className = "save-status saved"; this.#saveStatusEl.className = "save-status saved";
this.#saveStatusEl.innerHTML = "<span>\u2713</span><span>Saved</span>"; this.#saveStatusEl.innerHTML = "<span></span><span>Saved</span>";
} }
} }

View File

@ -117,7 +117,7 @@ const styles = css`
} }
.checkbox.checked::after { .checkbox.checked::after {
content: "\u2713"; content: "";
color: white; color: white;
font-size: 10px; font-size: 10px;
font-weight: 700; font-weight: 700;
@ -224,11 +224,11 @@ export class FolkPackingList extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\uD83C\uDF92</span> <span>🎒</span>
<span>Packing List</span> <span>Packing List</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="progress-info"> <div class="progress-info">

View File

@ -171,16 +171,16 @@ export class FolkPiano extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="piano-container" data-drag> <div class="piano-container" data-drag>
<div class="loading"> <div class="loading">
<span>\u{1F3B9}</span> <span>🎹</span>
<span>Loading Shared Piano...</span> <span>Loading Shared Piano...</span>
</div> </div>
<div class="error hidden"> <div class="error hidden">
<span>\u{1F3B9}</span> <span>🎹</span>
<span class="error-message">Failed to load piano</span> <span class="error-message">Failed to load piano</span>
<button class="retry-btn">Retry</button> <button class="retry-btn">Retry</button>
</div> </div>
<div class="minimized hidden"> <div class="minimized hidden">
<span>\u{1F3B9} Shared Piano</span> <span>🎹 Shared Piano</span>
</div> </div>
<iframe <iframe
class="piano-iframe" class="piano-iframe"
@ -190,7 +190,7 @@ export class FolkPiano extends FolkShape {
style="opacity: 0;" style="opacity: 0;"
></iframe> ></iframe>
<div class="controls"> <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>
</div> </div>
`; `;
@ -231,14 +231,14 @@ export class FolkPiano extends FolkShape {
minimizeBtn?.addEventListener("click", (e) => { minimizeBtn?.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
this.isMinimized = !this.#isMinimized; this.isMinimized = !this.#isMinimized;
minimizeBtn.textContent = this.#isMinimized ? "\u{1F53D}" : "\u{1F53C}"; minimizeBtn.textContent = this.#isMinimized ? "🔽" : "🔼";
}); });
// Click minimized view to expand // Click minimized view to expand
this.#minimizedEl?.addEventListener("click", (e) => { this.#minimizedEl?.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
this.isMinimized = false; this.isMinimized = false;
if (minimizeBtn) minimizeBtn.textContent = "\u{1F53C}"; if (minimizeBtn) minimizeBtn.textContent = "🔼";
}); });
// Retry button // Retry button

View File

@ -91,7 +91,7 @@ const styles = css`
} }
.message.streaming::after { .message.streaming::after {
content: "\u258C"; content: "";
animation: blink 1s infinite; animation: blink 1s infinite;
} }
@ -262,18 +262,18 @@ export class FolkPrompt extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\u{1F4AC}</span> <span>💬</span>
<span>AI Prompt</span> <span>AI Prompt</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="clear-btn" title="Clear chat">Clear</button> <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> </div>
<div class="content"> <div class="content">
<div class="messages"> <div class="messages">
<div class="placeholder"> <div class="placeholder">
<span class="placeholder-icon">\u{1F916}</span> <span class="placeholder-icon">🤖</span>
<span>Ask me anything!</span> <span>Ask me anything!</span>
<span style="font-size: 11px;">I can help with code, writing, analysis, and more</span> <span style="font-size: 11px;">I can help with code, writing, analysis, and more</span>
</div> </div>
@ -287,7 +287,7 @@ export class FolkPrompt extends FolkShape {
</select> </select>
<div class="prompt-row"> <div class="prompt-row">
<textarea class="prompt-input" placeholder="Type your message..." rows="2"></textarea> <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> </div>
</div> </div>
@ -418,7 +418,7 @@ export class FolkPrompt extends FolkShape {
if (this.#messages.length === 0 && !this.#error) { if (this.#messages.length === 0 && !this.#error) {
this.#messagesEl.innerHTML = ` this.#messagesEl.innerHTML = `
<div class="placeholder"> <div class="placeholder">
<span class="placeholder-icon">\u{1F916}</span> <span class="placeholder-icon">🤖</span>
<span>Ask me anything!</span> <span>Ask me anything!</span>
<span style="font-size: 11px;">I can help with code, writing, analysis, and more</span> <span style="font-size: 11px;">I can help with code, writing, analysis, and more</span>
</div> </div>

View File

@ -363,6 +363,16 @@ export class FolkShape extends FolkElement {
return this.#readonlyRect; 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) { handleEvent(event: PointerEvent | KeyboardEvent | TouchEvent) {
// Handle touch events for mobile drag support // Handle touch events for mobile drag support
if (event instanceof TouchEvent) { if (event instanceof TouchEvent) {
@ -390,7 +400,7 @@ export class FolkShape extends FolkElement {
if (event.type === "touchmove" && this.#isTouchDragging && event.touches.length === 1) { if (event.type === "touchmove" && this.#isTouchDragging && event.touches.length === 1) {
const touch = event.touches[0]; const touch = event.touches[0];
if (this.#lastTouchPos) { if (this.#lastTouchPos) {
const zoom = window.visualViewport?.scale ?? 1; const zoom = (window.visualViewport?.scale ?? 1) * this.#getParentScale();
const moveDelta = { const moveDelta = {
x: (touch.clientX - this.#lastTouchPos.x) / zoom, x: (touch.clientX - this.#lastTouchPos.x) / zoom,
y: (touch.clientY - this.#lastTouchPos.y) / zoom, y: (touch.clientY - this.#lastTouchPos.y) / zoom,
@ -478,7 +488,7 @@ export class FolkShape extends FolkElement {
}; };
} else if (event.type === "pointermove") { } else if (event.type === "pointermove") {
if (!target) return; if (!target) return;
const zoom = window.visualViewport?.scale ?? 1; const zoom = (window.visualViewport?.scale ?? 1) * this.#getParentScale();
moveDelta = { moveDelta = {
x: event.movementX / zoom, x: event.movementX / zoom,
y: event.movementY / zoom, y: event.movementY / zoom,
@ -659,9 +669,10 @@ export class FolkShape extends FolkElement {
/** /**
* After moving, push this shape away from any overlapping siblings. * 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; const parent = this.parentElement;
if (!parent) return; if (!parent) return;
@ -680,21 +691,25 @@ export class FolkShape extends FolkElement {
if (!overlapX || !overlapY) continue; if (!overlapX || !overlapY) continue;
// Distance to clear on each side // Distance to clear on each side (4 possible escape directions)
const clearRight = (other.x + other.w + gap) - me.x; // push me right of other const clearRight = (other.x + other.w + gap) - me.x;
const clearLeft = other.x - (me.x + me.w + gap); // push me left of other (negative) const clearLeft = other.x - (me.x + me.w + gap);
const clearDown = (other.y + other.h + gap) - me.y; // push me below other const clearDown = (other.y + other.h + gap) - me.y;
const clearUp = other.y - (me.y + me.h + gap); // push me above other (negative) const clearUp = other.y - (me.y + me.h + gap);
// Pick push direction per axis based on movement direction // Pick the direction with smallest absolute displacement
const pushX = dx >= 0 ? clearRight : clearLeft; const candidates = [
const pushY = dy >= 0 ? clearDown : clearUp; { 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 (best.axis === "x") {
if (Math.abs(pushX) <= Math.abs(pushY)) { this.#rect.x += best.d;
this.#rect.x += pushX;
} else { } else {
this.#rect.y += pushY; this.#rect.y += best.d;
} }
me.x = this.#rect.x; me.x = this.#rect.x;

View File

@ -370,13 +370,13 @@ const PLATFORM_CONFIG: Record<
SocialPlatform, SocialPlatform,
{ icon: string; label: string; color: string } { 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" }, linkedin: { icon: "in", label: "LinkedIn", color: "#0A66C2" },
instagram: { icon: "\ud83d\udcf7", label: "Instagram", color: "#E4405F" }, instagram: { icon: "📷", label: "Instagram", color: "#E4405F" },
youtube: { icon: "\u25b6", label: "YouTube", color: "#FF0000" }, youtube: { icon: "", label: "YouTube", color: "#FF0000" },
threads: { icon: "@", label: "Threads", color: "#000000" }, threads: { icon: "@", label: "Threads", color: "#000000" },
bluesky: { icon: "\ud83e\ude77", label: "Bluesky", color: "#0085FF" }, bluesky: { icon: "🩷", label: "Bluesky", color: "#0085FF" },
tiktok: { icon: "\u266b", label: "TikTok", color: "#010101" }, tiktok: { icon: "", label: "TikTok", color: "#010101" },
facebook: { icon: "f", label: "Facebook", color: "#1877F2" }, facebook: { icon: "f", label: "Facebook", color: "#1877F2" },
}; };
@ -561,8 +561,8 @@ export class FolkSocialPost extends FolkShape {
<span class="platform-name">${config.label}</span> <span class="platform-name">${config.label}</span>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="edit-btn" title="Edit">\u270F</button> <button class="edit-btn" title="Edit"></button>
<button class="close-btn" title="Remove">\u00D7</button> <button class="close-btn" title="Remove">×</button>
</div> </div>
</div> </div>
<div class="body"> <div class="body">
@ -572,7 +572,7 @@ export class FolkSocialPost extends FolkShape {
</div> </div>
<div class="media-preview"> <div class="media-preview">
<div class="media-placeholder"> <div class="media-placeholder">
<span class="icon">\ud83d\uddbc</span> <span class="icon">🖼</span>
<span>No media</span> <span>No media</span>
</div> </div>
</div> </div>
@ -580,7 +580,7 @@ export class FolkSocialPost extends FolkShape {
</div> </div>
<div class="footer"> <div class="footer">
<div class="schedule"> <div class="schedule">
<span class="schedule-icon">\ud83d\udcc5</span> <span class="schedule-icon">📅</span>
<span class="schedule-time" <span class="schedule-time"
>${this.#formatSchedule(this.#scheduledAt)}</span >${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>'" />`; 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 { } else {
const mediaIcons: Record<string, string> = { const mediaIcons: Record<string, string> = {
image: "\ud83d\uddbc", image: "🖼",
video: "\ud83c\udfac", video: "🎬",
carousel: "\ud83d\udcf8", carousel: "📸",
reel: "\ud83c\udfac", reel: "🎬",
short: "\ud83c\udfac", short: "🎬",
story: "\ud83d\udcf1", story: "📱",
}; };
const icon = mediaIcons[this.#postType] || "\ud83d\uddbc"; const icon = mediaIcons[this.#postType] || "🖼";
const label = const label =
this.#postType === "text" ? "No media" : `${this.#postType} media`; this.#postType === "text" ? "No media" : `${this.#postType} media`;
this.#mediaPreviewEl.innerHTML = `<div class="media-placeholder"><span class="icon">${icon}</span><span>${label}</span></div>`; this.#mediaPreviewEl.innerHTML = `<div class="media-placeholder"><span class="icon">${icon}</span><span>${label}</span></div>`;

View File

@ -340,7 +340,7 @@ export class FolkSplat extends FolkShape {
container.innerHTML = this.#gallerySplats.map((s) => ` container.innerHTML = this.#gallerySplats.map((s) => `
<div class="gallery-item" data-slug="${s.slug}"> <div class="gallery-item" data-slug="${s.slug}">
<div class="title">${this.#escapeHtml(s.title)}</div> <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> </div>
`).join(""); `).join("");

View File

@ -302,12 +302,12 @@ export class FolkTokenLedger extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\uD83D\uDCDC</span> <span>📜</span>
<span class="name">Token Ledger</span> <span class="name">Token Ledger</span>
<span class="symbol"></span> <span class="symbol"></span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="ledger-body"> <div class="ledger-body">
@ -470,7 +470,7 @@ export class FolkTokenLedger extends FolkShape {
<div class="holder-item"> <div class="holder-item">
<div class="holder-info"> <div class="holder-info">
<div class="holder-icon" style="background: ${info.isEscrow ? '#f59e0b' : this.#holderColor(holder)}"> <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> <div>
<div class="holder-name"> <div class="holder-name">

View File

@ -160,7 +160,7 @@ export class FolkTokenMint extends FolkShape {
#totalSupply = 1000; #totalSupply = 1000;
#issuedSupply = 0; #issuedSupply = 0;
#tokenColor = "#8b5cf6"; #tokenColor = "#8b5cf6";
#tokenIcon = "\uD83E\uDE99"; #tokenIcon = "🪙";
#createdBy = ""; #createdBy = "";
#createdAt = new Date().toISOString(); #createdAt = new Date().toISOString();
@ -243,7 +243,7 @@ export class FolkTokenMint extends FolkShape {
<span class="symbol">${this.#tokenSymbol}</span> <span class="symbol">${this.#tokenSymbol}</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="mint-body"> <div class="mint-body">

View File

@ -318,11 +318,11 @@ export class FolkTranscription extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\u{1F3A4}</span> <span>🎤</span>
<span>Transcription</span> <span>Transcription</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
@ -337,14 +337,14 @@ export class FolkTranscription extends FolkShape {
</div> </div>
<div class="transcript-area"> <div class="transcript-area">
<div class="placeholder"> <div class="placeholder">
<span class="placeholder-icon">\u{1F3A4}</span> <span class="placeholder-icon">🎤</span>
<span>Click the record button to start</span> <span>Click the record button to start</span>
<span style="font-size: 11px;">Uses your browser's speech recognition</span> <span style="font-size: 11px;">Uses your browser's speech recognition</span>
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
<button class="action-btn copy-btn">\u{1F4CB} Copy</button> <button class="action-btn copy-btn">📋 Copy</button>
<button class="action-btn clear-btn">\u{1F5D1} Clear</button> <button class="action-btn clear-btn">🗑 Clear</button>
</div> </div>
</div> </div>
`; `;
@ -544,7 +544,7 @@ export class FolkTranscription extends FolkShape {
if (this.#segments.length === 0) { if (this.#segments.length === 0) {
this.#transcriptArea.innerHTML = ` this.#transcriptArea.innerHTML = `
<div class="placeholder"> <div class="placeholder">
<span class="placeholder-icon">\u{1F3A4}</span> <span class="placeholder-icon">🎤</span>
<span>Click the record button to start</span> <span>Click the record button to start</span>
<span style="font-size: 11px;">Uses your browser's speech recognition</span> <span style="font-size: 11px;">Uses your browser's speech recognition</span>
</div> </div>

View File

@ -307,16 +307,16 @@ export class FolkVideoChat extends FolkShape {
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">
<span>\u{1F4F9}</span> <span>📹</span>
<span>Video Chat</span> <span>Video Chat</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<div class="join-screen"> <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-title">Join Video Call</span>
<span class="join-subtitle">Enter a room name to start or join a 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..." /> <input type="text" class="room-input" placeholder="Room name..." />
@ -402,10 +402,10 @@ export class FolkVideoChat extends FolkShape {
</div> </div>
</div> </div>
<div class="controls"> <div class="controls">
<button class="control-btn secondary" id="mute-btn" title="Mute">\u{1F50A}</button> <button class="control-btn secondary" id="mute-btn" title="Mute">🔊</button>
<button class="control-btn secondary" id="video-btn" title="Toggle Video">\u{1F4F7}</button> <button class="control-btn secondary" id="video-btn" title="Toggle Video">📷</button>
<button class="control-btn secondary" id="record-btn" title="Record">\u{23FA}</button> <button class="control-btn secondary" id="record-btn" title="Record"></button>
<button class="control-btn danger" id="leave-btn" title="Leave Call">\u{1F4DE}</button> <button class="control-btn danger" id="leave-btn" title="Leave Call">📞</button>
</div> </div>
`; `;
@ -464,7 +464,7 @@ export class FolkVideoChat extends FolkShape {
track.enabled = !this.#isMuted; track.enabled = !this.#isMuted;
}); });
} }
btn.textContent = this.#isMuted ? "\u{1F507}" : "\u{1F50A}"; btn.textContent = this.#isMuted ? "🔇" : "🔊";
btn.classList.toggle("muted", this.#isMuted); btn.classList.toggle("muted", this.#isMuted);
} }
@ -475,7 +475,7 @@ export class FolkVideoChat extends FolkShape {
track.enabled = !this.#isVideoOff; 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); btn.classList.toggle("muted", this.#isVideoOff);
video.style.opacity = this.#isVideoOff ? "0.3" : "1"; video.style.opacity = this.#isVideoOff ? "0.3" : "1";
} }

View File

@ -513,7 +513,7 @@ export class FolkVideoGen extends FolkShape {
if (!this.#videoArea) return; if (!this.#videoArea) return;
this.#videoArea.innerHTML = ` this.#videoArea.innerHTML = `
<div class="error">${this.#escapeHtml(this.#error || "Unknown error")}</div> <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) { if (this.#videos.length === 0) {
this.#videoArea.innerHTML = ` this.#videoArea.innerHTML = `
<div class="placeholder"> <div class="placeholder">
<span class="placeholder-icon">\u{1F3AC}</span> <span class="placeholder-icon">🎬</span>
<span>Upload an image and describe the motion</span> <span>Upload an image and describe the motion</span>
</div> </div>
`; `;

View File

@ -250,7 +250,7 @@ export class FolkWorkflowBlock extends FolkShape {
#blockType: BlockType = "action"; #blockType: BlockType = "action";
#label = "Block"; #label = "Block";
#icon = "\u{2699}"; #icon = "";
#state: BlockState = "idle"; #state: BlockState = "idle";
#inputs: Port[] = []; #inputs: Port[] = [];
#outputs: Port[] = []; #outputs: Port[] = [];
@ -324,8 +324,8 @@ export class FolkWorkflowBlock extends FolkShape {
<span class="block-label">${this.#escapeHtml(this.#label)}</span> <span class="block-label">${this.#escapeHtml(this.#label)}</span>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="settings-btn" title="Settings">\u{2699}</button> <button class="settings-btn" title="Settings"></button>
<button class="close-btn" title="Close">\u00D7</button> <button class="close-btn" title="Close">×</button>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
@ -336,7 +336,7 @@ export class FolkWorkflowBlock extends FolkShape {
<span class="status-dot idle"></span> <span class="status-dot idle"></span>
<span class="status-text">Idle</span> <span class="status-text">Idle</span>
</div> </div>
<button class="run-btn">\u25B6 Run</button> <button class="run-btn"> Run</button>
</div> </div>
`; `;
@ -382,16 +382,16 @@ export class FolkWorkflowBlock extends FolkShape {
#updateIcon() { #updateIcon() {
switch (this.#blockType) { switch (this.#blockType) {
case "trigger": case "trigger":
this.#icon = "\u26A1"; this.#icon = "";
break; break;
case "action": case "action":
this.#icon = "\u2699"; this.#icon = "";
break; break;
case "condition": case "condition":
this.#icon = "\u2753"; this.#icon = "";
break; break;
case "output": case "output":
this.#icon = "\u{1F4E4}"; this.#icon = "📤";
break; break;
} }
} }

View File

@ -235,7 +235,7 @@ export class FolkBookReader extends HTMLElement {
${this.getStyles()} ${this.getStyles()}
<div class="reader-container"> <div class="reader-container">
<div class="rapp-nav"> <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> <span class="rapp-nav__title">${this.escapeHtml(this._title)}</span>
${this._author ? `<span class="rapp-nav__subtitle">by ${this.escapeHtml(this._author)}</span>` : ""} ${this._author ? `<span class="rapp-nav__subtitle">by ${this.escapeHtml(this._author)}</span>` : ""}
<span class="rapp-nav__meta"> <span class="rapp-nav__meta">

View File

@ -62,9 +62,9 @@ class FolkCalendarView extends HTMLElement {
private getMoonEmoji(phase: string): string { private getMoonEmoji(phase: string): string {
const map: Record<string, string> = { const map: Record<string, string> = {
new_moon: "\u{1F311}", waxing_crescent: "\u{1F312}", first_quarter: "\u{1F313}", new_moon: "🌑", waxing_crescent: "🌒", first_quarter: "🌓",
waxing_gibbous: "\u{1F314}", full_moon: "\u{1F315}", waning_gibbous: "\u{1F316}", waxing_gibbous: "🌔", full_moon: "🌕", waning_gibbous: "🌖",
last_quarter: "\u{1F317}", waning_crescent: "\u{1F318}", last_quarter: "🌗", waning_crescent: "🌘",
}; };
return map[phase] || ""; 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>` : ""} ${this.error ? `<div style="color:#ef5350;text-align:center;padding:8px">${this.esc(this.error)}</div>` : ""}
<div class="rapp-nav"> <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> <span class="rapp-nav__title">${monthName} ${year}</span>
<button class="toggle-btn ${this.showLunar ? "active" : ""}" id="toggle-lunar">\u{1F319} Lunar</button> <button class="toggle-btn ${this.showLunar ? "active" : ""}" id="toggle-lunar">🌙 Lunar</button>
<button class="rapp-nav__back" id="next">\u2192</button> <button class="rapp-nav__back" id="next"></button>
</div> </div>
${this.sources.length > 0 ? `<div class="sources"> ${this.sources.length > 0 ? `<div class="sources">
@ -191,10 +191,10 @@ class FolkCalendarView extends HTMLElement {
return ` return `
<div class="event-modal" id="modal-overlay"> <div class="event-modal" id="modal-overlay">
<div class="modal-content"> <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> <div class="modal-title">${this.esc(e.title)}</div>
${e.description ? `<div class="modal-field">${this.esc(e.description)}</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.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.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>` : ""} ${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>` : ""}

View File

@ -391,7 +391,7 @@ routes.get("/", (c) => {
export const calModule: RSpaceModule = { export const calModule: RSpaceModule = {
id: "rcal", id: "rcal",
name: "rCal", name: "rCal",
icon: "\u{1F4C5}", icon: "📅",
description: "Temporal coordination calendar with lunar, solar, and seasonal systems", description: "Temporal coordination calendar with lunar, solar, and seasonal systems",
routes, routes,
standaloneDomain: "rcal.online", standaloneDomain: "rcal.online",

View File

@ -80,12 +80,12 @@ class FolkCartShop extends HTMLElement {
<div class="rapp-nav"> <div class="rapp-nav">
<span class="rapp-nav__title">Shop</span> <span class="rapp-nav__title">Shop</span>
<div class="tabs"> <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 === 'catalog' ? 'active' : ''}" data-view="catalog">📦 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 === 'orders' ? 'active' : ''}" data-view="orders">📋 Orders (${this.orders.length})</button>
</div> </div>
</div> </div>
${this.loading ? `<div class="loading">\u23F3 Loading...</div>` : ${this.loading ? `<div class="loading"> Loading...</div>` :
this.view === "catalog" ? this.renderCatalog() : this.renderOrders()} 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> <h3 class="card-title">${this.esc(order.artifact_title || "Order")}</h3>
<div class="card-meta"> <div class="card-meta">
${order.provider_name ? `Provider: ${this.esc(order.provider_name)}` : ""} ${order.provider_name ? `Provider: ${this.esc(order.provider_name)}` : ""}
${order.quantity > 1 ? ` \u2022 Qty: ${order.quantity}` : ""} ${order.quantity > 1 ? ` Qty: ${order.quantity}` : ""}
</div> </div>
<span class="status status-${order.status}">${order.status}</span> <span class="status status-${order.status}">${order.status}</span>
</div> </div>

View File

@ -457,7 +457,7 @@ routes.get("/", (c) => {
export const cartModule: RSpaceModule = { export const cartModule: RSpaceModule = {
id: "rcart", id: "rcart",
name: "rCart", name: "rCart",
icon: "\u{1F6D2}", icon: "🛒",
description: "Cosmolocal print-on-demand shop", description: "Cosmolocal print-on-demand shop",
routes, routes,
standaloneDomain: "rcart.online", standaloneDomain: "rcart.online",

View File

@ -42,9 +42,9 @@ class FolkChoicesDashboard extends HTMLElement {
private render() { private render() {
const typeIcons: Record<string, string> = { const typeIcons: Record<string, string> = {
"folk-choice-vote": "\u2611", "folk-choice-vote": "",
"folk-choice-rank": "\uD83D\uDCCA", "folk-choice-rank": "📊",
"folk-choice-spider": "\uD83D\uDD78", "folk-choice-spider": "🕸",
}; };
const typeLabels: Record<string, string> = { const typeLabels: Record<string, string> = {
"folk-choice-vote": "Poll", "folk-choice-vote": "Poll",
@ -78,7 +78,7 @@ class FolkChoicesDashboard extends HTMLElement {
<div class="rapp-nav"> <div class="rapp-nav">
<span class="rapp-nav__title">Choices</span> <span class="rapp-nav__title">Choices</span>
<div class="create-btns"> <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>
</div> </div>
@ -87,14 +87,14 @@ class FolkChoicesDashboard extends HTMLElement {
Create them there and they'll appear here for quick access. Create them there and they'll appear here for quick access.
</div> </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)} this.choices.length === 0 ? this.renderEmpty() : this.renderGrid(typeIcons, typeLabels)}
`; `;
} }
private renderEmpty(): string { private renderEmpty(): string {
return `<div class="empty"> return `<div class="empty">
<div class="empty-icon">\u2611</div> <div class="empty-icon"></div>
<p>No choices in this space yet.</p> <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> <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>`; </div>`;
@ -104,7 +104,7 @@ class FolkChoicesDashboard extends HTMLElement {
return `<div class="grid"> return `<div class="grid">
${this.choices.map((ch) => ` ${this.choices.map((ch) => `
<a class="card" href="/${this.space}/rspace"> <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> <div class="card-type">${labels[ch.type] || ch.type}</div>
<h3 class="card-title">${this.esc(ch.title)}</h3> <h3 class="card-title">${this.esc(ch.title)}</h3>
<div class="card-meta"> <div class="card-meta">

View File

@ -136,7 +136,7 @@ routes.get("/", (c) => {
export const dataModule: RSpaceModule = { export const dataModule: RSpaceModule = {
id: "rdata", id: "rdata",
name: "rData", name: "rData",
icon: "\u{1F4CA}", icon: "📊",
description: "Privacy-first analytics for the r* ecosystem", description: "Privacy-first analytics for the r* ecosystem",
routes, routes,
standaloneDomain: "rdata.online", standaloneDomain: "rdata.online",

View File

@ -69,24 +69,24 @@ class FolkFileBrowser extends HTMLElement {
} }
private mimeIcon(mime: string): string { private mimeIcon(mime: string): string {
if (mime?.startsWith("image/")) return "\uD83D\uDDBC\uFE0F"; if (mime?.startsWith("image/")) return "🖼️";
if (mime?.startsWith("video/")) return "\uD83C\uDFA5"; if (mime?.startsWith("video/")) return "🎥";
if (mime?.startsWith("audio/")) return "\uD83C\uDFB5"; if (mime?.startsWith("audio/")) return "🎵";
if (mime?.includes("pdf")) return "\uD83D\uDCC4"; if (mime?.includes("pdf")) return "📄";
if (mime?.includes("zip") || mime?.includes("tar") || mime?.includes("gz")) return "\uD83D\uDCE6"; if (mime?.includes("zip") || mime?.includes("tar") || mime?.includes("gz")) return "📦";
if (mime?.includes("text") || mime?.includes("json") || mime?.includes("xml")) return "\uD83D\uDCDD"; if (mime?.includes("text") || mime?.includes("json") || mime?.includes("xml")) return "📝";
return "\uD83D\uDCC1"; return "📁";
} }
private cardTypeIcon(type: string): string { private cardTypeIcon(type: string): string {
const icons: Record<string, string> = { const icons: Record<string, string> = {
note: "\uD83D\uDCDD", note: "📝",
idea: "\uD83D\uDCA1", idea: "💡",
task: "\u2705", task: "",
reference: "\uD83D\uDD17", reference: "🔗",
quote: "\uD83D\uDCAC", quote: "💬",
}; };
return icons[type] || "\uD83D\uDCDD"; return icons[type] || "📝";
} }
private async handleUpload(e: Event) { private async handleUpload(e: Event) {
@ -258,8 +258,8 @@ class FolkFileBrowser extends HTMLElement {
</style> </style>
<div class="tabs"> <div class="tabs">
<div class="tab-btn" data-tab="files">\uD83D\uDCC1 Files</div> <div class="tab-btn" data-tab="files">📁 Files</div>
<div class="tab-btn" data-tab="cards">\uD83C\uDFB4 Memory Cards</div> <div class="tab-btn" data-tab="cards">🎴 Memory Cards</div>
</div> </div>
${filesActive ? this.renderFilesTab() : this.renderCardsTab()} ${filesActive ? this.renderFilesTab() : this.renderCardsTab()}

View File

@ -382,7 +382,7 @@ routes.get("/", (c) => {
export const filesModule: RSpaceModule = { export const filesModule: RSpaceModule = {
id: "rfiles", id: "rfiles",
name: "rFiles", name: "rFiles",
icon: "\uD83D\uDCC1", icon: "📁",
description: "File sharing, share links, and memory cards", description: "File sharing, share links, and memory cards",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -150,10 +150,10 @@ class FolkForumDashboard extends HTMLElement {
} }
private logStepIcon(status: string): string { private logStepIcon(status: string): string {
if (status === "success") return "\u2705"; if (status === "success") return "";
if (status === "error") return "\u274C"; if (status === "error") return "";
if (status === "running") return "\u23F3"; if (status === "running") return "";
return "\u23ED\uFE0F"; return "⏭️";
} }
private render() { private render() {
@ -264,7 +264,7 @@ class FolkForumDashboard extends HTMLElement {
return ` return `
<div class="rapp-nav"> <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> <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>` : ""} ${inst.status !== "destroyed" ? `<button class="danger" data-action="destroy" data-id="${inst.id}">Destroy</button>` : ""}
</div> </div>
@ -275,7 +275,7 @@ class FolkForumDashboard extends HTMLElement {
<div class="detail-title">${this.esc(inst.name)}</div> <div class="detail-title">${this.esc(inst.name)}</div>
<div style="margin-top:4px">${this.statusBadge(inst.status)}</div> <div style="margin-top:4px">${this.statusBadge(inst.status)}</div>
</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> </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>` : ""} ${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>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>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>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>
<div class="logs-section"> <div class="logs-section">
@ -309,7 +309,7 @@ class FolkForumDashboard extends HTMLElement {
private renderCreate(): string { private renderCreate(): string {
return ` return `
<div class="rapp-nav"> <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> <span class="rapp-nav__title">Deploy New Forum</span>
</div> </div>
@ -319,17 +319,17 @@ class FolkForumDashboard extends HTMLElement {
<div class="pricing"> <div class="pricing">
<div class="price-card selected" data-size="cx22"> <div class="price-card selected" data-size="cx22">
<div class="price-name">Starter</div> <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 &middot; 4 GB &middot; ~500 users</div> <div class="price-specs">2 vCPU &middot; 4 GB &middot; ~500 users</div>
</div> </div>
<div class="price-card" data-size="cx32"> <div class="price-card" data-size="cx32">
<div class="price-name">Standard</div> <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 &middot; 8 GB &middot; ~2000 users</div> <div class="price-specs">4 vCPU &middot; 8 GB &middot; ~2000 users</div>
</div> </div>
<div class="price-card" data-size="cx42"> <div class="price-card" data-size="cx42">
<div class="price-name">Performance</div> <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 &middot; 16 GB &middot; ~10k users</div> <div class="price-specs">8 vCPU &middot; 16 GB &middot; ~10k users</div>
</div> </div>
</div> </div>

View File

@ -173,7 +173,7 @@ routes.get("/", (c) => {
export const forumModule: RSpaceModule = { export const forumModule: RSpaceModule = {
id: "rforum", id: "rforum",
name: "rForum", name: "rForum",
icon: "\uD83D\uDCAC", icon: "💬",
description: "Deploy and manage Discourse forums", description: "Deploy and manage Discourse forums",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -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})"/> <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("")} ${[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 - 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}" 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]}"/>`; <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)} ${renderSufficiencyBadge(score, layout.width - 70, 10)}
</svg> </svg>
<div class="controls"> <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>
<div class="legend"> <div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#10b981"></div> Inflow</div> <div class="legend-item"><div class="legend-dot" style="background:#10b981"></div> Inflow</div>

View File

@ -243,7 +243,7 @@ routes.get("/flow/:flowId", (c) => {
export const fundsModule: RSpaceModule = { export const fundsModule: RSpaceModule = {
id: "rfunds", id: "rfunds",
name: "rFunds", name: "rFunds",
icon: "\uD83C\uDF0A", icon: "🌊",
description: "Budget flows, river visualization, and treasury management", description: "Budget flows, river visualization, and treasury management",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -597,7 +597,7 @@ routes.get("/", (c) => {
export const inboxModule: RSpaceModule = { export const inboxModule: RSpaceModule = {
id: "rinbox", id: "rinbox",
name: "rInbox", name: "rInbox",
icon: "\u{1F4E8}", icon: "📨",
description: "Collaborative email with multisig approval", description: "Collaborative email with multisig approval",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -165,7 +165,7 @@ class FolkMapViewer extends HTMLElement {
${this.rooms.length > 0 ? this.rooms.map((r) => ` ${this.rooms.length > 0 ? this.rooms.map((r) => `
<div class="room-card" data-room="${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> <span class="room-name">${this.esc(r)}</span>
</div> </div>
`).join("") : ""} `).join("") : ""}
@ -181,14 +181,14 @@ class FolkMapViewer extends HTMLElement {
const shareUrl = `${window.location.origin}/${this.space}/maps/${this.room}`; const shareUrl = `${window.location.origin}/${this.space}/maps/${this.room}`;
return ` return `
<div class="rapp-nav"> <div class="rapp-nav">
<button class="rapp-nav__back" data-back="lobby">\u2190 Rooms</button> <button class="rapp-nav__back" data-back="lobby"> Rooms</button>
<span class="rapp-nav__title">\u{1F5FA} ${this.esc(this.room)}</span> <span class="rapp-nav__title">🗺 ${this.esc(this.room)}</span>
<span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}"></span> <span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}"></span>
</div> </div>
<div class="map-container"> <div class="map-container">
<div class="map-placeholder"> <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 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>Connect the MapLibre GL library to display the interactive map.</p>
<p style="font-size:12px;color:#555">WebSocket sync: ${this.syncStatus}</p> <p style="font-size:12px;color:#555">WebSocket sync: ${this.syncStatus}</p>

View File

@ -165,7 +165,7 @@ routes.get("/:room", (c) => {
export const mapsModule: RSpaceModule = { export const mapsModule: RSpaceModule = {
id: "rmaps", id: "rmaps",
name: "rMaps", name: "rMaps",
icon: "\u{1F5FA}", icon: "🗺",
description: "Real-time collaborative location sharing and indoor/outdoor maps", description: "Real-time collaborative location sharing and indoor/outdoor maps",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -112,7 +112,7 @@ class FolkGraphViewer extends HTMLElement {
<div class="graph-canvas"> <div class="graph-canvas">
<div class="placeholder"> <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 style="font-size:16px">Community Relationship Graph</p>
<p>Connect the force-directed layout engine to visualize your network.</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> <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 => ` ${this.workspaces.map(ws => `
<div class="ws-card"> <div class="ws-card">
<div class="ws-name">${this.esc(ws.name || ws.slug)}</div> <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> </div>
`).join("")} `).join("")}
</div> </div>

View File

@ -232,7 +232,7 @@ routes.get("/", (c) => {
export const networkModule: RSpaceModule = { export const networkModule: RSpaceModule = {
id: "rnetwork", id: "rnetwork",
name: "rNetwork", name: "rNetwork",
icon: "\u{1F310}", icon: "🌐",
description: "Community relationship graph visualization with CRM sync", description: "Community relationship graph visualization with CRM sync",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -429,14 +429,14 @@ class FolkNotesApp extends HTMLElement {
private getNoteIcon(type: string): string { private getNoteIcon(type: string): string {
switch (type) { switch (type) {
case "NOTE": return "\u{1F4DD}"; case "NOTE": return "📝";
case "CODE": return "\u{1F4BB}"; case "CODE": return "💻";
case "BOOKMARK": return "\u{1F517}"; case "BOOKMARK": return "🔗";
case "IMAGE": return "\u{1F5BC}"; case "IMAGE": return "🖼";
case "AUDIO": return "\u{1F3A4}"; case "AUDIO": return "🎤";
case "FILE": return "\u{1F4CE}"; case "FILE": return "📎";
case "CLIP": return "\u2702\uFE0F"; case "CLIP": return "✂️";
default: return "\u{1F4C4}"; default: return "📄";
} }
} }
@ -573,7 +573,7 @@ class FolkNotesApp extends HTMLElement {
: ""; : "";
return ` return `
<div class="rapp-nav"> <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> <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> <button class="rapp-nav__btn" id="create-note">+ New Note</button>
</div> </div>
@ -606,7 +606,7 @@ class FolkNotesApp extends HTMLElement {
const isAutomerge = !!(this.doc?.items?.[n.id]); const isAutomerge = !!(this.doc?.items?.[n.id]);
return ` return `
<div class="rapp-nav"> <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 ${isAutomerge
? `<input class="editable-title" id="note-title-input" value="${this.esc(n.title)}" placeholder="Note title...">` ? `<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>` : `<span class="rapp-nav__title">${this.getNoteIcon(n.type)} ${this.esc(n.title)}</span>`

View File

@ -377,7 +377,7 @@ routes.get("/", (c) => {
export const notesModule: RSpaceModule = { export const notesModule: RSpaceModule = {
id: "rnotes", id: "rnotes",
name: "rNotes", name: "rNotes",
icon: "\u{1F4DD}", icon: "📝",
description: "Notebooks with rich-text notes, voice transcription, and collaboration", description: "Notebooks with rich-text notes, voice transcription, and collaboration",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -327,7 +327,7 @@ class FolkPhotoGallery extends HTMLElement {
const album = this.selectedAlbum!; const album = this.selectedAlbum!;
return ` return `
<div class="rapp-nav"> <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> <span class="rapp-nav__title">${this.esc(album.albumName)}</span>
<div class="rapp-nav__actions"> <div class="rapp-nav__actions">
<a class="rapp-nav__btn rapp-nav__btn--secondary" href="${this.getImmichUrl()}/albums/${album.id}" target="_blank" rel="noopener"> <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 ` return `
<div class="lightbox" data-lightbox> <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)}"> <img src="${this.originalUrl(asset.id)}" alt="${this.esc(asset.originalFileName)}">
<div class="lightbox-info"> <div class="lightbox-info">
${asset.originalFileName} ${asset.originalFileName}

View File

@ -184,7 +184,7 @@ routes.get("/", (c) => {
export const socialsModule: RSpaceModule = { export const socialsModule: RSpaceModule = {
id: "rsocials", id: "rsocials",
name: "rSocials", name: "rSocials",
icon: "\u{1F4E2}", icon: "📢",
description: "Federated social feed aggregator for communities", description: "Federated social feed aggregator for communities",
routes, routes,
standaloneDomain: "rsocials.online", standaloneDomain: "rsocials.online",

View File

@ -63,9 +63,9 @@ class FolkSwagDesigner extends HTMLElement {
private render() { private render() {
const products = [ const products = [
{ id: "sticker", name: "Sticker Sheet", icon: "\u{1F4CB}", desc: "A4 vinyl stickers" }, { id: "sticker", name: "Sticker Sheet", icon: "📋", desc: "A4 vinyl stickers" },
{ id: "poster", name: "Poster (A3)", icon: "\u{1F5BC}", desc: "A3 art print" }, { id: "poster", name: "Poster (A3)", icon: "🖼", desc: "A3 art print" },
{ id: "tee", name: "T-Shirt", icon: "\u{1F455}", desc: "12x16\" DTG print" }, { id: "tee", name: "T-Shirt", icon: "👕", desc: "12x16\" DTG print" },
]; ];
this.shadow.innerHTML = ` this.shadow.innerHTML = `
@ -117,33 +117,33 @@ class FolkSwagDesigner extends HTMLElement {
<div class="upload-area ${this.imagePreview ? 'has-image' : ''}"> <div class="upload-area ${this.imagePreview ? 'has-image' : ''}">
${this.imagePreview ${this.imagePreview
? `<img class="preview-img" src="${this.imagePreview}" alt="Preview">` ? `<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/*"> <input type="file" accept="image/*">
</div> </div>
<input class="title-input" type="text" placeholder="Design title" value="${this.esc(this.designTitle)}"> <input class="title-input" type="text" placeholder="Design title" value="${this.esc(this.designTitle)}">
<button class="generate-btn" ${!this.imageFile || this.generating ? 'disabled' : ''}> <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> </button>
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""} ${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
${this.artifact ? ` ${this.artifact ? `
<div class="result"> <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"> <div class="result-meta">
${this.esc(this.artifact.spec?.product_type || "")} \u2022 ${this.esc(this.artifact.spec?.product_type || "")}
${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm \u2022 ${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm
${this.artifact.spec?.dpi}dpi ${this.artifact.spec?.dpi}dpi
</div> </div>
<div class="result-actions"> <div class="result-actions">
${Object.entries(this.artifact.render_targets || {}).map(([key, target]: [string, any]) => ` ${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("")} `).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> </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> <pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
</div>` : ""} </div>` : ""}
`; `;

View File

@ -244,7 +244,7 @@ routes.get("/", (c) => {
export const swagModule: RSpaceModule = { export const swagModule: RSpaceModule = {
id: "rswag", id: "rswag",
name: "rSwag", name: "rSwag",
icon: "\u{1F3A8}", icon: "🎨",
description: "Design print-ready swag: stickers, posters, tees", description: "Design print-ready swag: stickers, posters, tees",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -311,7 +311,7 @@ class FolkRoutePlanner extends HTMLElement {
// Compute crossing angle // Compute crossing angle
const dot = Math.abs(tA.dx * tB.dx + tA.dy * tB.dy); const dot = Math.abs(tA.dx * tB.dx + tA.dy * tB.dy);
const angle = Math.acos(Math.min(1, dot)) * (180 / Math.PI); 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; return svg;
} }
@ -494,8 +494,8 @@ class FolkRoutePlanner extends HTMLElement {
<div class="svg-container">${this.renderSVG()}</div> <div class="svg-container">${this.renderSVG()}</div>
${this.renderInfo()} ${this.renderInfo()}
<div class="math-note"> <div class="math-note">
Conic: Ax\u00B2+Bxy+Cy\u00B2+Dx+Ey+F=0 | Discriminant \u0394=B\u00B2\u22124AC | Conic: Ax²+Bxy+Cy²+Dx+Ey+F=0 | Discriminant Δ=B²4AC |
Intersection via Sylvester resultant \u2192 degree-4 polynomial \u2192 companion matrix eigenvalues Intersection via Sylvester resultant degree-4 polynomial companion matrix eigenvalues
</div> </div>
` : ""} ` : ""}

View File

@ -124,10 +124,10 @@ class FolkTripsPlanner extends HTMLElement {
<div class="trip-card" data-trip="${t.id}"> <div class="trip-card" data-trip="${t.id}">
<div class="trip-name">${this.esc(t.title)}</div> <div class="trip-name">${this.esc(t.title)}</div>
<div class="trip-meta"> <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"} ${t.start_date ? new Date(t.start_date).toLocaleDateString() : "No dates"}
</div> </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> <span class="trip-status status-${t.status || "PLANNING"}">${t.status || "PLANNING"}</span>
</div> </div>
`).join("")} `).join("")}
@ -143,7 +143,7 @@ class FolkTripsPlanner extends HTMLElement {
const t = this.trip; const t = this.trip;
return ` return `
<div class="rapp-nav"> <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="rapp-nav__title">${this.esc(t.title)}</span>
<span class="trip-status status-${t.status || "PLANNING"}">${t.status || "PLANNING"}</span> <span class="trip-status status-${t.status || "PLANNING"}">${t.status || "PLANNING"}</span>
</div> </div>
@ -165,7 +165,7 @@ class FolkTripsPlanner extends HTMLElement {
return ` return `
<div class="section-title">Trip Details</div> <div class="section-title">Trip Details</div>
<div class="item-row"><span class="item-title">${t.description || "No description"}</span></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 ? ` ${t.budget_total ? `
<div class="section-title">Budget</div> <div class="section-title">Budget</div>
<div class="item-row" style="flex-direction:column;align-items:stretch"> <div class="item-row" style="flex-direction:column;align-items:stretch">
@ -177,17 +177,17 @@ class FolkTripsPlanner extends HTMLElement {
</div> </div>
` : ""} ` : ""}
<div class="section-title">Summary</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": case "destinations":
return (t.destinations || []).length > 0 return (t.destinations || []).length > 0
? (t.destinations || []).map((d: any) => ` ? (t.destinations || []).map((d: any) => `
<div class="item-row"> <div class="item-row">
<span style="font-size:20px">\u{1F4CD}</span> <span style="font-size:20px">📍</span>
<div style="flex:1"> <div style="flex:1">
<div class="item-title">${this.esc(d.name)}</div> <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>
</div> </div>
`).join("") `).join("")
@ -211,7 +211,7 @@ class FolkTripsPlanner extends HTMLElement {
<span class="badge">${b.type || "OTHER"}</span> <span class="badge">${b.type || "OTHER"}</span>
<div style="flex:1"> <div style="flex:1">
<div class="item-title">${this.esc(b.provider || "Booking")}</div> <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>
</div> </div>
`).join("") `).join("")

View File

@ -269,7 +269,7 @@ routes.get("/", (c) => {
export const tripsModule: RSpaceModule = { export const tripsModule: RSpaceModule = {
id: "rtrips", id: "rtrips",
name: "rTrips", name: "rTrips",
icon: "\u{2708}\u{FE0F}", icon: "✈️",
description: "Collaborative trip planner with itinerary, bookings, and expense splitting", description: "Collaborative trip planner with itinerary, bookings, and expense splitting",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -207,7 +207,7 @@ routes.get("/", (c) => {
export const tubeModule: RSpaceModule = { export const tubeModule: RSpaceModule = {
id: "rtube", id: "rtube",
name: "rTube", name: "rTube",
icon: "\u{1F3AC}", icon: "🎬",
description: "Community video hosting & live streaming", description: "Community video hosting & live streaming",
routes, routes,
landingPage: renderLanding, landingPage: renderLanding,

View File

@ -231,7 +231,7 @@ class FolkVoteDashboard extends HTMLElement {
const s = this.selectedSpace!; const s = this.selectedSpace!;
return ` return `
<div class="rapp-nav"> <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> <span class="rapp-nav__title">${this.esc(s.name)} Proposals</span>
</div> </div>
${this.proposals.length === 0 ? '<div class="empty">No proposals yet.</div>' : ""} ${this.proposals.length === 0 ? '<div class="empty">No proposals yet.</div>' : ""}
@ -264,7 +264,7 @@ class FolkVoteDashboard extends HTMLElement {
const p = this.selectedProposal!; const p = this.selectedProposal!;
return ` return `
<div class="rapp-nav"> <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="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> <span class="badge" style="background:${this.getStatusColor(p.status)}20;color:${this.getStatusColor(p.status)};margin-left:8px">${p.status}</span>
</div> </div>

View File

@ -343,7 +343,7 @@ routes.get("/", (c) => {
export const voteModule: RSpaceModule = { export const voteModule: RSpaceModule = {
id: "rvote", id: "rvote",
name: "rVote", name: "rVote",
icon: "\u{1F5F3}", icon: "🗳",
description: "Conviction voting engine for collaborative governance", description: "Conviction voting engine for collaborative governance",
routes, routes,
standaloneDomain: "rvote.online", standaloneDomain: "rvote.online",

View File

@ -110,7 +110,7 @@ routes.get("/", (c) => {
export const walletModule: RSpaceModule = { export const walletModule: RSpaceModule = {
id: "rwallet", id: "rwallet",
name: "rWallet", name: "rWallet",
icon: "\uD83D\uDCB0", icon: "💰",
description: "Multichain Safe wallet visualization and treasury management", description: "Multichain Safe wallet visualization and treasury management",
routes, routes,
standaloneDomain: "rwallet.online", standaloneDomain: "rwallet.online",

View File

@ -167,8 +167,8 @@ class FolkWorkBoard extends HTMLElement {
${this.workspaces.length > 0 ? `<div class="workspace-grid"> ${this.workspaces.length > 0 ? `<div class="workspace-grid">
${this.workspaces.map(ws => ` ${this.workspaces.map(ws => `
<div class="workspace-card" data-ws="${ws.slug}"> <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-name">${this.esc(ws.icon || "📋")} ${this.esc(ws.name)}</div>
<div class="ws-meta">${ws.task_count || 0} tasks \u00B7 ${ws.member_count || 0} members</div> <div class="ws-meta">${ws.task_count || 0} tasks · ${ws.member_count || 0} members</div>
</div> </div>
`).join("")} `).join("")}
</div>` : `<div class="empty"> </div>` : `<div class="empty">
@ -181,7 +181,7 @@ class FolkWorkBoard extends HTMLElement {
private renderBoard(): string { private renderBoard(): string {
return ` return `
<div class="rapp-nav"> <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> <span class="rapp-nav__title">${this.esc(this.workspaceSlug)}</span>
<button class="rapp-nav__btn" id="create-task">+ New Task</button> <button class="rapp-nav__btn" id="create-task">+ New Task</button>
</div> </div>
@ -213,7 +213,7 @@ class FolkWorkBoard extends HTMLElement {
${(task.labels || []).map((l: string) => `<span class="badge">${this.esc(l)}</span>`).join("")} ${(task.labels || []).map((l: string) => `<span class="badge">${this.esc(l)}</span>`).join("")}
</div> </div>
<div class="move-btns"> <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>
</div> </div>
`; `;

View File

@ -233,7 +233,7 @@ routes.get("/", (c) => {
export const workModule: RSpaceModule = { export const workModule: RSpaceModule = {
id: "rwork", id: "rwork",
name: "rWork", name: "rWork",
icon: "\u{1F4CB}", icon: "📋",
description: "Kanban workspace boards for collaborative task management", description: "Kanban workspace boards for collaborative task management",
routes, routes,
standaloneDomain: "rwork.online", standaloneDomain: "rwork.online",

View File

@ -73,7 +73,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "thread", postType: "thread",
stepNumber: 1, stepNumber: 1,
content: 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: "", mediaUrl: "",
mediaType: "", mediaType: "",
scheduledAt: "2026-02-21T09:00:00", scheduledAt: "2026-02-21T09:00:00",
@ -93,7 +93,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "article", postType: "article",
stepNumber: 2, stepNumber: 2,
content: 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: "", mediaUrl: "",
mediaType: "image", mediaType: "image",
scheduledAt: "2026-02-22T11:00:00", scheduledAt: "2026-02-22T11:00:00",
@ -113,7 +113,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "carousel", postType: "carousel",
stepNumber: 3, stepNumber: 3,
content: 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: "", mediaUrl: "",
mediaType: "carousel", mediaType: "carousel",
scheduledAt: "2026-02-23T14:00:00", scheduledAt: "2026-02-23T14:00:00",
@ -141,7 +141,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "video", postType: "video",
stepNumber: 4, stepNumber: 4,
content: 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: "", mediaUrl: "",
mediaType: "video", mediaType: "video",
scheduledAt: "2026-02-24T10:00:00", scheduledAt: "2026-02-24T10:00:00",
@ -166,7 +166,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "thread", postType: "thread",
stepNumber: 5, stepNumber: 5,
content: 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: "", mediaUrl: "",
mediaType: "image", mediaType: "image",
scheduledAt: "2026-02-24T10:15:00", scheduledAt: "2026-02-24T10:15:00",
@ -192,7 +192,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "text", postType: "text",
stepNumber: 6, stepNumber: 6,
content: 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: "", mediaUrl: "",
mediaType: "image", mediaType: "image",
scheduledAt: "2026-02-24T11:00:00", scheduledAt: "2026-02-24T11:00:00",
@ -220,7 +220,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "reel", postType: "reel",
stepNumber: 7, stepNumber: 7,
content: 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: "", mediaUrl: "",
mediaType: "video", mediaType: "video",
scheduledAt: "2026-02-25T12:00:00", scheduledAt: "2026-02-25T12:00:00",
@ -246,7 +246,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "text", postType: "text",
stepNumber: 8, stepNumber: 8,
content: 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: "", mediaUrl: "",
mediaType: "", mediaType: "",
scheduledAt: "2026-02-25T14:00:00", scheduledAt: "2026-02-25T14:00:00",
@ -266,7 +266,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "text", postType: "text",
stepNumber: 9, stepNumber: 9,
content: 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: "", mediaUrl: "",
mediaType: "", mediaType: "",
scheduledAt: "2026-02-25T15:00:00", scheduledAt: "2026-02-25T15:00:00",
@ -286,7 +286,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
height: 160, height: 160,
rotation: 0, rotation: 0,
content: 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 2125, 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, height: 36,
rotation: 0, rotation: 0,
content: content:
"### \ud83d\udce3 Phase 1: Pre-Launch Hype (Feb 21\u201323)", "### 📣 Phase 1: Pre-Launch Hype (Feb 2123)",
}, },
{ {
id: "label-phase2", id: "label-phase2",
@ -311,7 +311,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
width: COL_WIDTH * 3 + COL_GAP * 2, width: COL_WIDTH * 3 + COL_GAP * 2,
height: 36, height: 36,
rotation: 0, rotation: 0,
content: "### \ud83d\ude80 Phase 2: Launch Day (Feb 24)", content: "### 🚀 Phase 2: Launch Day (Feb 24)",
}, },
{ {
id: "label-phase3", id: "label-phase3",
@ -322,7 +322,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
height: 36, height: 36,
rotation: 0, rotation: 0,
content: content:
"### \ud83d\udce1 Phase 3: Amplification (Feb 25)", "### 📡 Phase 3: Amplification (Feb 25)",
}, },
// ──────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────

View File

@ -422,7 +422,7 @@ const DEMO_SHAPES: Record<string, unknown>[] = [
totalSupply: 100, totalSupply: 100,
issuedSupply: 100, issuedSupply: 100,
tokenColor: "#6d28d9", tokenColor: "#6d28d9",
tokenIcon: "\uD83D\uDDF3\uFE0F", tokenIcon: "🗳️",
createdBy: "Maya", createdBy: "Maya",
createdAt: "2026-06-15T10:00:00.000Z", createdAt: "2026-06-15T10:00:00.000Z",
}, },
@ -458,7 +458,7 @@ const DEMO_SHAPES: Record<string, unknown>[] = [
totalSupply: 500, totalSupply: 500,
issuedSupply: 155, issuedSupply: 155,
tokenColor: "#059669", tokenColor: "#059669",
tokenIcon: "\u2B50", tokenIcon: "",
createdBy: "Maya", createdBy: "Maya",
createdAt: "2026-06-20T14:00:00.000Z", createdAt: "2026-06-20T14:00:00.000Z",
}, },

View File

@ -270,11 +270,11 @@ export class RStackIdentity extends HTMLElement {
<div class="dropdown-header">${displayName}</div> <div class="dropdown-header">${displayName}</div>
${notifsHTML} ${notifsHTML}
<div class="dropdown-divider"></div> <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-email"> Add Email</button>
<button class="dropdown-item" data-action="add-device">\uD83D\uDCF1 Add Second Device</button> <button class="dropdown-item" data-action="add-device">📱 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-recovery">🛡 Add Social Recovery</button>
<div class="dropdown-divider"></div> <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>
</div> </div>
`; `;
@ -447,7 +447,7 @@ export class RStackIdentity extends HTMLElement {
autoResolveSpace(data.token, data.username || ""); autoResolveSpace(data.token, data.username || "");
} catch (err: any) { } catch (err: any) {
btn.disabled = false; 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."; errEl.textContent = err.name === "NotAllowedError" ? "Authentication was cancelled." : err.message || "Authentication failed.";
} }
}; };

View File

@ -111,9 +111,9 @@ export class RStackSpaceSwitcher extends HTMLElement {
#visibilityInfo(s: SpaceInfo): { cls: string; label: string } { #visibilityInfo(s: SpaceInfo): { cls: string; label: string } {
const v = s.visibility || "public_read"; const v = s.visibility || "public_read";
if (v === "members_only") return { cls: "vis-private", label: "\uD83D\uDD12" }; if (v === "members_only") return { cls: "vis-private", label: "🔒" };
if (v === "authenticated") return { cls: "vis-permissioned", label: "\uD83D\uDD11" }; if (v === "authenticated") return { cls: "vis-permissioned", label: "🔑" };
return { cls: "vis-public", label: "\uD83D\uDD13" }; return { cls: "vis-public", label: "🔓" };
} }
#renderMenu(menu: HTMLElement, current: string) { #renderMenu(menu: HTMLElement, current: string) {
@ -146,7 +146,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
return ` return `
<a class="item ${vis.cls} ${s.slug === current ? "active" : ""}" <a class="item ${vis.cls} ${s.slug === current ? "active" : ""}"
href="${rspaceNavUrl(s.slug, moduleId)}"> 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-name">${s.name}</span>
<span class="item-vis ${vis.cls}">${vis.label}</span> <span class="item-vis ${vis.cls}">${vis.label}</span>
</a>`; </a>`;
@ -164,7 +164,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
return ` return `
<a class="item ${vis.cls} ${s.slug === current ? "active" : ""}" <a class="item ${vis.cls} ${s.slug === current ? "active" : ""}"
href="${rspaceNavUrl(s.slug, moduleId)}"> 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-name">${s.name}</span>
<span class="item-vis ${vis.cls}">${vis.label}</span> <span class="item-vis ${vis.cls}">${vis.label}</span>
</a>`; </a>`;
@ -182,7 +182,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
const pending = s.pendingRequest; const pending = s.pendingRequest;
return ` return `
<div class="item item--discover ${vis.cls}"> <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-name">${s.name}</span>
<span class="item-vis ${vis.cls}">${vis.label}</span> <span class="item-vis ${vis.cls}">${vis.label}</span>
${pending ${pending

View File

@ -367,7 +367,7 @@ export class RStackTabBar extends HTMLElement {
const contained = containedSet.has(f.id); const contained = containedSet.has(f.id);
return `<span class="io-chip io-chip--out ${contained ? "io-chip--contained" : ""}" return `<span class="io-chip io-chip--out ${contained ? "io-chip--contained" : ""}"
style="--chip-color:${FLOW_COLORS[f.kind]}" title="${f.description || f.name}"> 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>`; </span>`;
}).join(""); }).join("");
@ -431,7 +431,7 @@ export class RStackTabBar extends HTMLElement {
const scrubberHtml = ` const scrubberHtml = `
<div class="time-scrubber"> <div class="time-scrubber">
<button class="scrubber-playpause" id="scrubber-playpause" title="${this.#simPlaying ? "Pause" : "Play"}"> <button class="scrubber-playpause" id="scrubber-playpause" title="${this.#simPlaying ? "Pause" : "Play"}">
${this.#simPlaying ? "\u23F8" : "\u25B6"} ${this.#simPlaying ? "⏸" : "▶"}
</button> </button>
<input type="range" class="scrubber-range" id="scrubber-range" <input type="range" class="scrubber-range" id="scrubber-range"
min="0.1" max="5" step="0.1" value="${this.#simSpeed}" /> min="0.1" max="5" step="0.1" value="${this.#simSpeed}" />
@ -451,7 +451,7 @@ export class RStackTabBar extends HTMLElement {
</div> </div>
<div class="stack-legend">${legendHtml}</div> <div class="stack-legend">${legendHtml}</div>
${scrubberHtml} ${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() : ""} ${this.#flowDialogOpen ? this.#renderFlowDialog() : ""}
</div> </div>
`; `;
@ -486,7 +486,7 @@ export class RStackTabBar extends HTMLElement {
<div class="flow-dialog-header">New Flow</div> <div class="flow-dialog-header">New Flow</div>
<div class="flow-dialog-route"> <div class="flow-dialog-route">
<span class="flow-dialog-badge" style="background:${srcBadge?.color || "#94a3b8"}">${srcBadge?.badge || srcLayer.moduleId}</span> <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> <span class="flow-dialog-badge" style="background:${tgtBadge?.color || "#94a3b8"}">${tgtBadge?.badge || tgtLayer.moduleId}</span>
</div> </div>
<div class="flow-dialog-field"> <div class="flow-dialog-field">
@ -782,7 +782,7 @@ export class RStackTabBar extends HTMLElement {
scrubberPlaypause?.addEventListener("click", () => { scrubberPlaypause?.addEventListener("click", () => {
this.#simPlaying = !this.#simPlaying; this.#simPlaying = !this.#simPlaying;
scrubberPlaypause.textContent = this.#simPlaying ? "\u23F8" : "\u25B6"; scrubberPlaypause.textContent = this.#simPlaying ? "⏸" : "▶";
scrubberPlaypause.title = this.#simPlaying ? "Pause" : "Play"; scrubberPlaypause.title = this.#simPlaying ? "Pause" : "Play";
const state = this.#simPlaying ? "running" : "paused"; const state = this.#simPlaying ? "running" : "paused";
this.#shadow.querySelectorAll<HTMLElement>(".flow-particle").forEach(p => { this.#shadow.querySelectorAll<HTMLElement>(".flow-particle").forEach(p => {