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) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#renderUrl)}" alt="3D Render" />`;
} else {
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u2705</span><span>Script generated (see Script tab)</span></div>';
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon"></span><span>Script generated (see Script tab)</span></div>';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -341,7 +341,7 @@ export class FolkFreeCAD extends FolkShape {
if (this.#previewUrl) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#previewUrl)}" alt="CAD Preview" />`;
} else {
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u2705</span><span>Model generated! Download files below.</span></div>';
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon"></span><span>Model generated! Download files below.</span></div>';
}
}

View File

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

View File

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

View File

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

View File

@ -434,14 +434,14 @@ export class FolkKiCAD extends FolkShape {
if (this.#schematicSvg) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#schematicSvg)}" alt="Schematic" />`;
} else {
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u{1F4CB}</span><span>Schematic will appear here</span></div>';
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">📋</span><span>Schematic will appear here</span></div>';
}
break;
case "board":
if (this.#boardSvg) {
this.#previewArea.innerHTML = `<img src="${this.#escapeHtml(this.#boardSvg)}" alt="Board Layout" />`;
} else {
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u{1F4DF}</span><span>Board layout will appear here</span></div>';
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">📟</span><span>Board layout will appear here</span></div>';
}
break;
case "drc":
@ -451,14 +451,14 @@ export class FolkKiCAD extends FolkShape {
this.#previewArea.innerHTML = `
<div class="drc-results" style="padding:12px">
<h4 style="margin:0 0 8px">${passed
? '<span class="pass">\u2705 DRC Passed</span>'
: `<span class="fail">\u274C ${violations.length} Violation(s)</span>`
? '<span class="pass"> DRC Passed</span>'
: `<span class="fail"> ${violations.length} Violation(s)</span>`
}</h4>
${violations.map((v: any) => `<p style="margin:4px 0;font-size:12px">\u2022 ${this.#escapeHtml(v.message || v)}</p>`).join("")}
${violations.map((v: any) => `<p style="margin:4px 0;font-size:12px"> ${this.#escapeHtml(v.message || v)}</p>`).join("")}
</div>
`;
} else {
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon">\u2705</span><span>DRC results will appear here</span></div>';
this.#previewArea.innerHTML = '<div class="placeholder"><span class="placeholder-icon"></span><span>DRC results will appear here</span></div>';
}
break;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -340,7 +340,7 @@ export class FolkSplat extends FolkShape {
container.innerHTML = this.#gallerySplats.map((s) => `
<div class="gallery-item" data-slug="${s.slug}">
<div class="title">${this.#escapeHtml(s.title)}</div>
<div class="meta">${s.file_format} \u2022 ${(s.file_size_bytes / 1024 / 1024).toFixed(1)}MB</div>
<div class="meta">${s.file_format} ${(s.file_size_bytes / 1024 / 1024).toFixed(1)}MB</div>
</div>
`).join("");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -235,7 +235,7 @@ export class FolkBookReader extends HTMLElement {
${this.getStyles()}
<div class="reader-container">
<div class="rapp-nav">
<a class="rapp-nav__back" href="/${window.location.pathname.split('/')[1]}/rbooks">\u2190 Library</a>
<a class="rapp-nav__back" href="/${window.location.pathname.split('/')[1]}/rbooks"> Library</a>
<span class="rapp-nav__title">${this.escapeHtml(this._title)}</span>
${this._author ? `<span class="rapp-nav__subtitle">by ${this.escapeHtml(this._author)}</span>` : ""}
<span class="rapp-nav__meta">

View File

@ -62,9 +62,9 @@ class FolkCalendarView extends HTMLElement {
private getMoonEmoji(phase: string): string {
const map: Record<string, string> = {
new_moon: "\u{1F311}", waxing_crescent: "\u{1F312}", first_quarter: "\u{1F313}",
waxing_gibbous: "\u{1F314}", full_moon: "\u{1F315}", waning_gibbous: "\u{1F316}",
last_quarter: "\u{1F317}", waning_crescent: "\u{1F318}",
new_moon: "🌑", waxing_crescent: "🌒", first_quarter: "🌓",
waxing_gibbous: "🌔", full_moon: "🌕", waning_gibbous: "🌖",
last_quarter: "🌗", waning_crescent: "🌘",
};
return map[phase] || "";
}
@ -120,10 +120,10 @@ class FolkCalendarView extends HTMLElement {
${this.error ? `<div style="color:#ef5350;text-align:center;padding:8px">${this.esc(this.error)}</div>` : ""}
<div class="rapp-nav">
<button class="rapp-nav__back" id="prev">\u2190</button>
<button class="rapp-nav__back" id="prev"></button>
<span class="rapp-nav__title">${monthName} ${year}</span>
<button class="toggle-btn ${this.showLunar ? "active" : ""}" id="toggle-lunar">\u{1F319} Lunar</button>
<button class="rapp-nav__back" id="next">\u2192</button>
<button class="toggle-btn ${this.showLunar ? "active" : ""}" id="toggle-lunar">🌙 Lunar</button>
<button class="rapp-nav__back" id="next"></button>
</div>
${this.sources.length > 0 ? `<div class="sources">
@ -191,10 +191,10 @@ class FolkCalendarView extends HTMLElement {
return `
<div class="event-modal" id="modal-overlay">
<div class="modal-content">
<button class="modal-close" id="modal-close">\u2715</button>
<button class="modal-close" id="modal-close"></button>
<div class="modal-title">${this.esc(e.title)}</div>
${e.description ? `<div class="modal-field">${this.esc(e.description)}</div>` : ""}
<div class="modal-field">When: ${new Date(e.start_time).toLocaleString()}${e.end_time ? ` \u2014 ${new Date(e.end_time).toLocaleString()}` : ""}</div>
<div class="modal-field">When: ${new Date(e.start_time).toLocaleString()}${e.end_time ? ` ${new Date(e.end_time).toLocaleString()}` : ""}</div>
${e.location_name ? `<div class="modal-field">Where: ${this.esc(e.location_name)}</div>` : ""}
${e.source_name ? `<div class="modal-field">Source: ${this.esc(e.source_name)}</div>` : ""}
${e.is_virtual ? `<div class="modal-field">Virtual: ${this.esc(e.virtual_platform || "")} ${e.virtual_url ? `<a href="${e.virtual_url}" target="_blank" style="color:#6366f1">Join</a>` : ""}</div>` : ""}

View File

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

View File

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

View File

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

View File

@ -42,9 +42,9 @@ class FolkChoicesDashboard extends HTMLElement {
private render() {
const typeIcons: Record<string, string> = {
"folk-choice-vote": "\u2611",
"folk-choice-rank": "\uD83D\uDCCA",
"folk-choice-spider": "\uD83D\uDD78",
"folk-choice-vote": "",
"folk-choice-rank": "📊",
"folk-choice-spider": "🕸",
};
const typeLabels: Record<string, string> = {
"folk-choice-vote": "Poll",
@ -78,7 +78,7 @@ class FolkChoicesDashboard extends HTMLElement {
<div class="rapp-nav">
<span class="rapp-nav__title">Choices</span>
<div class="create-btns">
<a class="create-btn" href="/${this.space}/rspace" title="Open canvas to create choices">\u2795 New on Canvas</a>
<a class="create-btn" href="/${this.space}/rspace" title="Open canvas to create choices"> New on Canvas</a>
</div>
</div>
@ -87,14 +87,14 @@ class FolkChoicesDashboard extends HTMLElement {
Create them there and they'll appear here for quick access.
</div>
${this.loading ? `<div class="loading">\u23F3 Loading choices...</div>` :
${this.loading ? `<div class="loading"> Loading choices...</div>` :
this.choices.length === 0 ? this.renderEmpty() : this.renderGrid(typeIcons, typeLabels)}
`;
}
private renderEmpty(): string {
return `<div class="empty">
<div class="empty-icon">\u2611</div>
<div class="empty-icon"></div>
<p>No choices in this space yet.</p>
<p>Open the <a href="/${this.space}/rspace" style="color:#818cf8">canvas</a> and use the Poll, Rank, or Spider buttons to create one.</p>
</div>`;
@ -104,7 +104,7 @@ class FolkChoicesDashboard extends HTMLElement {
return `<div class="grid">
${this.choices.map((ch) => `
<a class="card" href="/${this.space}/rspace">
<div class="card-icon">${icons[ch.type] || "\u2611"}</div>
<div class="card-icon">${icons[ch.type] || ""}</div>
<div class="card-type">${labels[ch.type] || ch.type}</div>
<h3 class="card-title">${this.esc(ch.title)}</h3>
<div class="card-meta">

View File

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

View File

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

View File

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

View File

@ -150,10 +150,10 @@ class FolkForumDashboard extends HTMLElement {
}
private logStepIcon(status: string): string {
if (status === "success") return "\u2705";
if (status === "error") return "\u274C";
if (status === "running") return "\u23F3";
return "\u23ED\uFE0F";
if (status === "success") return "";
if (status === "error") return "";
if (status === "running") return "";
return "⏭️";
}
private render() {
@ -264,7 +264,7 @@ class FolkForumDashboard extends HTMLElement {
return `
<div class="rapp-nav">
<button class="rapp-nav__back" data-action="back">\u2190 Forums</button>
<button class="rapp-nav__back" data-action="back"> Forums</button>
<span class="rapp-nav__title">${this.esc(inst.name)}</span>
${inst.status !== "destroyed" ? `<button class="danger" data-action="destroy" data-id="${inst.id}">Destroy</button>` : ""}
</div>
@ -275,7 +275,7 @@ class FolkForumDashboard extends HTMLElement {
<div class="detail-title">${this.esc(inst.name)}</div>
<div style="margin-top:4px">${this.statusBadge(inst.status)}</div>
</div>
${inst.status === "active" ? `<a href="https://${inst.domain}" target="_blank" style="color:#64b5f6;font-size:13px">\u2197 Open Forum</a>` : ""}
${inst.status === "active" ? `<a href="https://${inst.domain}" target="_blank" style="color:#64b5f6;font-size:13px"> Open Forum</a>` : ""}
</div>
${inst.error_message ? `<div style="background:#7a2a2a33;border:1px solid #7a2a2a;padding:10px;border-radius:6px;margin-bottom:16px;font-size:13px;color:#ef5350">${this.esc(inst.error_message)}</div>` : ""}
@ -286,7 +286,7 @@ class FolkForumDashboard extends HTMLElement {
<div class="detail-item"><label>Region</label><div class="value">${inst.region}</div></div>
<div class="detail-item"><label>Server Size</label><div class="value">${inst.size}</div></div>
<div class="detail-item"><label>Admin Email</label><div class="value">${inst.admin_email || "—"}</div></div>
<div class="detail-item"><label>SSL</label><div class="value">${inst.ssl_provisioned ? "\u2705 Active" : "\u23F3 Pending"}</div></div>
<div class="detail-item"><label>SSL</label><div class="value">${inst.ssl_provisioned ? "✅ Active" : "⏳ Pending"}</div></div>
</div>
<div class="logs-section">
@ -309,7 +309,7 @@ class FolkForumDashboard extends HTMLElement {
private renderCreate(): string {
return `
<div class="rapp-nav">
<button class="rapp-nav__back" data-action="back">\u2190 Forums</button>
<button class="rapp-nav__back" data-action="back"> Forums</button>
<span class="rapp-nav__title">Deploy New Forum</span>
</div>
@ -319,17 +319,17 @@ class FolkForumDashboard extends HTMLElement {
<div class="pricing">
<div class="price-card selected" data-size="cx22">
<div class="price-name">Starter</div>
<div class="price-cost">\u20AC3.79/mo</div>
<div class="price-cost">3.79/mo</div>
<div class="price-specs">2 vCPU &middot; 4 GB &middot; ~500 users</div>
</div>
<div class="price-card" data-size="cx32">
<div class="price-name">Standard</div>
<div class="price-cost">\u20AC6.80/mo</div>
<div class="price-cost">6.80/mo</div>
<div class="price-specs">4 vCPU &middot; 8 GB &middot; ~2000 users</div>
</div>
<div class="price-card" data-size="cx42">
<div class="price-name">Performance</div>
<div class="price-cost">\u20AC13.80/mo</div>
<div class="price-cost">13.80/mo</div>
<div class="price-specs">8 vCPU &middot; 16 GB &middot; ~10k users</div>
</div>
</div>

View File

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

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})"/>
${[0, 1, 2].map((i) => `<rect x="${f.x}" y="${f.y + (f.riverWidth / 4) * i}" width="${f.segmentLength}" height="${f.riverWidth / 4}" fill="${colors[0]}" opacity="0.08" style="animation:waterFlow ${2 + i * 0.5}s linear infinite;animation-delay:${i * -0.6}s"/>`).join("")}
<text x="${f.x + f.segmentLength / 2}" y="${f.y - 12}" text-anchor="middle" fill="${COLORS.text}" font-size="13" font-weight="600">${esc(f.label)}</text>
<text x="${f.x + f.segmentLength / 2}" y="${f.y - 2}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">$${Math.floor(f.data.currentValue).toLocaleString()} / $${Math.floor(threshold).toLocaleString()} ${isSufficient ? "\u2728" : ""}</text>
<text x="${f.x + f.segmentLength / 2}" y="${f.y - 2}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">$${Math.floor(f.data.currentValue).toLocaleString()} / $${Math.floor(threshold).toLocaleString()} ${isSufficient ? "" : ""}</text>
<rect x="${f.x}" y="${f.y + f.riverWidth + 4}" width="${f.segmentLength}" height="3" rx="1.5" fill="#334155"/>
<rect x="${f.x}" y="${f.y + f.riverWidth + 4}" width="${f.segmentLength * fillRatio}" height="3" rx="1.5" fill="${colors[0]}"/>`;
}
@ -471,7 +471,7 @@ class FolkBudgetRiver extends HTMLElement {
${renderSufficiencyBadge(score, layout.width - 70, 10)}
</svg>
<div class="controls">
<button class="${this.simulating ? "active" : ""}" data-action="toggle-sim">${this.simulating ? "\u23F8 Pause" : "\u25B6 Simulate"}</button>
<button class="${this.simulating ? "active" : ""}" data-action="toggle-sim">${this.simulating ? "⏸ Pause" : "▶ Simulate"}</button>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#10b981"></div> Inflow</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -112,7 +112,7 @@ class FolkGraphViewer extends HTMLElement {
<div class="graph-canvas">
<div class="placeholder">
<p style="font-size:48px">\u{1F578}\u{FE0F}</p>
<p style="font-size:48px">🕸</p>
<p style="font-size:16px">Community Relationship Graph</p>
<p>Connect the force-directed layout engine to visualize your network.</p>
<p style="font-size:12px;color:#444">Automerge CRDT sync + d3-force layout</p>
@ -131,7 +131,7 @@ class FolkGraphViewer extends HTMLElement {
${this.workspaces.map(ws => `
<div class="ws-card">
<div class="ws-name">${this.esc(ws.name || ws.slug)}</div>
<div class="ws-meta">${ws.nodeCount || 0} nodes \u00B7 ${ws.edgeCount || 0} edges</div>
<div class="ws-meta">${ws.nodeCount || 0} nodes · ${ws.edgeCount || 0} edges</div>
</div>
`).join("")}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,9 +63,9 @@ class FolkSwagDesigner extends HTMLElement {
private render() {
const products = [
{ id: "sticker", name: "Sticker Sheet", icon: "\u{1F4CB}", desc: "A4 vinyl stickers" },
{ id: "poster", name: "Poster (A3)", icon: "\u{1F5BC}", desc: "A3 art print" },
{ id: "tee", name: "T-Shirt", icon: "\u{1F455}", desc: "12x16\" DTG print" },
{ id: "sticker", name: "Sticker Sheet", icon: "📋", desc: "A4 vinyl stickers" },
{ id: "poster", name: "Poster (A3)", icon: "🖼", desc: "A3 art print" },
{ id: "tee", name: "T-Shirt", icon: "👕", desc: "12x16\" DTG print" },
];
this.shadow.innerHTML = `
@ -117,33 +117,33 @@ class FolkSwagDesigner extends HTMLElement {
<div class="upload-area ${this.imagePreview ? 'has-image' : ''}">
${this.imagePreview
? `<img class="preview-img" src="${this.imagePreview}" alt="Preview">`
: `<div class="upload-label">\u{1F4C1} Click or drag to upload artwork (PNG, JPG, SVG)</div>`}
: `<div class="upload-label">📁 Click or drag to upload artwork (PNG, JPG, SVG)</div>`}
<input type="file" accept="image/*">
</div>
<input class="title-input" type="text" placeholder="Design title" value="${this.esc(this.designTitle)}">
<button class="generate-btn" ${!this.imageFile || this.generating ? 'disabled' : ''}>
${this.generating ? '\u23F3 Generating...' : '\u{1F680} Generate Print-Ready Files'}
${this.generating ? '⏳ Generating...' : '🚀 Generate Print-Ready Files'}
</button>
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
${this.artifact ? `
<div class="result">
<h3 class="result-title">\u2705 ${this.esc(this.artifact.payload?.title || "Artifact")}</h3>
<h3 class="result-title"> ${this.esc(this.artifact.payload?.title || "Artifact")}</h3>
<div class="result-meta">
${this.esc(this.artifact.spec?.product_type || "")} \u2022
${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm \u2022
${this.esc(this.artifact.spec?.product_type || "")}
${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm
${this.artifact.spec?.dpi}dpi
</div>
<div class="result-actions">
${Object.entries(this.artifact.render_targets || {}).map(([key, target]: [string, any]) => `
<a class="result-btn result-btn-primary" href="${target.url}" target="_blank">\u{2B07} Download ${target.format.toUpperCase()}</a>
<a class="result-btn result-btn-primary" href="${target.url}" target="_blank"> Download ${target.format.toUpperCase()}</a>
`).join("")}
<button class="result-btn result-btn-secondary" data-action="copy-json">\u{1F4CB} Copy Artifact JSON</button>
<button class="result-btn result-btn-secondary" data-action="copy-json">📋 Copy Artifact JSON</button>
</div>
<span class="json-toggle">Show artifact envelope \u25BC</span>
<span class="json-toggle">Show artifact envelope </span>
<pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
</div>` : ""}
`;

View File

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

View File

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

View File

@ -124,10 +124,10 @@ class FolkTripsPlanner extends HTMLElement {
<div class="trip-card" data-trip="${t.id}">
<div class="trip-name">${this.esc(t.title)}</div>
<div class="trip-meta">
${t.destination_count || 0} destinations \u00B7
${t.destination_count || 0} destinations ·
${t.start_date ? new Date(t.start_date).toLocaleDateString() : "No dates"}
</div>
${t.budget_total ? `<div class="trip-meta">Budget: $${parseFloat(t.budget_total).toFixed(0)} \u00B7 Spent: $${parseFloat(t.total_spent || 0).toFixed(0)}</div>` : ""}
${t.budget_total ? `<div class="trip-meta">Budget: $${parseFloat(t.budget_total).toFixed(0)} · Spent: $${parseFloat(t.total_spent || 0).toFixed(0)}</div>` : ""}
<span class="trip-status status-${t.status || "PLANNING"}">${t.status || "PLANNING"}</span>
</div>
`).join("")}
@ -143,7 +143,7 @@ class FolkTripsPlanner extends HTMLElement {
const t = this.trip;
return `
<div class="rapp-nav">
<button class="rapp-nav__back" data-back="list">\u2190 Trips</button>
<button class="rapp-nav__back" data-back="list"> Trips</button>
<span class="rapp-nav__title">${this.esc(t.title)}</span>
<span class="trip-status status-${t.status || "PLANNING"}">${t.status || "PLANNING"}</span>
</div>
@ -165,7 +165,7 @@ class FolkTripsPlanner extends HTMLElement {
return `
<div class="section-title">Trip Details</div>
<div class="item-row"><span class="item-title">${t.description || "No description"}</span></div>
${t.start_date ? `<div class="item-row"><span class="item-title">Dates: ${new Date(t.start_date).toLocaleDateString()} \u2014 ${t.end_date ? new Date(t.end_date).toLocaleDateString() : "open"}</span></div>` : ""}
${t.start_date ? `<div class="item-row"><span class="item-title">Dates: ${new Date(t.start_date).toLocaleDateString()} ${t.end_date ? new Date(t.end_date).toLocaleDateString() : "open"}</span></div>` : ""}
${t.budget_total ? `
<div class="section-title">Budget</div>
<div class="item-row" style="flex-direction:column;align-items:stretch">
@ -177,17 +177,17 @@ class FolkTripsPlanner extends HTMLElement {
</div>
` : ""}
<div class="section-title">Summary</div>
<div class="item-row"><span class="item-meta">${(t.destinations || []).length} destinations \u00B7 ${(t.itinerary || []).length} activities \u00B7 ${(t.bookings || []).length} bookings \u00B7 ${(t.packing || []).length} packing items</span></div>
<div class="item-row"><span class="item-meta">${(t.destinations || []).length} destinations · ${(t.itinerary || []).length} activities · ${(t.bookings || []).length} bookings · ${(t.packing || []).length} packing items</span></div>
`;
}
case "destinations":
return (t.destinations || []).length > 0
? (t.destinations || []).map((d: any) => `
<div class="item-row">
<span style="font-size:20px">\u{1F4CD}</span>
<span style="font-size:20px">📍</span>
<div style="flex:1">
<div class="item-title">${this.esc(d.name)}</div>
<div class="item-meta">${d.country || ""} ${d.arrival_date ? `\u00B7 ${new Date(d.arrival_date).toLocaleDateString()}` : ""}</div>
<div class="item-meta">${d.country || ""} ${d.arrival_date ? `· ${new Date(d.arrival_date).toLocaleDateString()}` : ""}</div>
</div>
</div>
`).join("")
@ -211,7 +211,7 @@ class FolkTripsPlanner extends HTMLElement {
<span class="badge">${b.type || "OTHER"}</span>
<div style="flex:1">
<div class="item-title">${this.esc(b.provider || "Booking")}</div>
<div class="item-meta">${b.confirmation_number ? `#${b.confirmation_number}` : ""} ${b.cost ? `\u00B7 $${parseFloat(b.cost).toFixed(0)}` : ""}</div>
<div class="item-meta">${b.confirmation_number ? `#${b.confirmation_number}` : ""} ${b.cost ? `· $${parseFloat(b.cost).toFixed(0)}` : ""}</div>
</div>
</div>
`).join("")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -167,8 +167,8 @@ class FolkWorkBoard extends HTMLElement {
${this.workspaces.length > 0 ? `<div class="workspace-grid">
${this.workspaces.map(ws => `
<div class="workspace-card" data-ws="${ws.slug}">
<div class="ws-name">${this.esc(ws.icon || "\u{1F4CB}")} ${this.esc(ws.name)}</div>
<div class="ws-meta">${ws.task_count || 0} tasks \u00B7 ${ws.member_count || 0} members</div>
<div class="ws-name">${this.esc(ws.icon || "📋")} ${this.esc(ws.name)}</div>
<div class="ws-meta">${ws.task_count || 0} tasks · ${ws.member_count || 0} members</div>
</div>
`).join("")}
</div>` : `<div class="empty">
@ -181,7 +181,7 @@ class FolkWorkBoard extends HTMLElement {
private renderBoard(): string {
return `
<div class="rapp-nav">
<button class="rapp-nav__back" data-back="list">\u2190 Workspaces</button>
<button class="rapp-nav__back" data-back="list"> Workspaces</button>
<span class="rapp-nav__title">${this.esc(this.workspaceSlug)}</span>
<button class="rapp-nav__btn" id="create-task">+ New Task</button>
</div>
@ -213,7 +213,7 @@ class FolkWorkBoard extends HTMLElement {
${(task.labels || []).map((l: string) => `<span class="badge">${this.esc(l)}</span>`).join("")}
</div>
<div class="move-btns">
${otherStatuses.map(s => `<button class="move-btn" data-move="${task.id}" data-to="${s}">\u2192 ${this.esc(s.replace(/_/g, " ").substring(0, 8))}</button>`).join("")}
${otherStatuses.map(s => `<button class="move-btn" data-move="${task.id}" data-to="${s}"> ${this.esc(s.replace(/_/g, " ").substring(0, 8))}</button>`).join("")}
</div>
</div>
`;

View File

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

View File

@ -73,7 +73,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "thread",
stepNumber: 1,
content:
"Something is growing in the mycelium... \ud83c\udf44\n\nFor the past 2 years, we've been building the infrastructure for a regenerative economy.\n\nOn Feb 24, we reveal everything.\n\nA thread on why the old financial system is composting itself \ud83e\uddf5\ud83d\udc47",
"Something is growing in the mycelium... 🍄\n\nFor the past 2 years, we've been building the infrastructure for a regenerative economy.\n\nOn Feb 24, we reveal everything.\n\nA thread on why the old financial system is composting itself 🧵👇",
mediaUrl: "",
mediaType: "",
scheduledAt: "2026-02-21T09:00:00",
@ -93,7 +93,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "article",
stepNumber: 2,
content:
"The regenerative finance movement isn't just about returns \u2014 it's about redesigning incentive structures from the ground up.\n\nIn this article, I break down why mycelial network theory offers the best model for decentralized economic coordination.\n\n3 key insights from 2 years of building MycoFi Earth...",
"The regenerative finance movement isn't just about returns it's about redesigning incentive structures from the ground up.\n\nIn this article, I break down why mycelial network theory offers the best model for decentralized economic coordination.\n\n3 key insights from 2 years of building MycoFi Earth...",
mediaUrl: "",
mediaType: "image",
scheduledAt: "2026-02-22T11:00:00",
@ -113,7 +113,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "carousel",
stepNumber: 3,
content:
"5 Ways Mycelium Networks Mirror the Future of Finance \ud83c\udf0d\ud83c\udf44\n\nSlide 1: The problem with extractive finance\nSlide 2: How mycelium redistributes nutrients\nSlide 3: Token-weighted funding circles\nSlide 4: Community governance that actually works\nSlide 5: Join the launch \u2014 Feb 24",
"5 Ways Mycelium Networks Mirror the Future of Finance 🌍🍄\n\nSlide 1: The problem with extractive finance\nSlide 2: How mycelium redistributes nutrients\nSlide 3: Token-weighted funding circles\nSlide 4: Community governance that actually works\nSlide 5: Join the launch Feb 24",
mediaUrl: "",
mediaType: "carousel",
scheduledAt: "2026-02-23T14:00:00",
@ -141,7 +141,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "video",
stepNumber: 4,
content:
"MycoFi Earth \u2014 Official Launch Video\n\nThe regenerative economy starts here. Watch how mycelial intelligence is reshaping finance, governance, and community coordination.\n\nFeaturing interviews with 12 builders from the ecosystem.\n\n[18:42]",
"MycoFi Earth Official Launch Video\n\nThe regenerative economy starts here. Watch how mycelial intelligence is reshaping finance, governance, and community coordination.\n\nFeaturing interviews with 12 builders from the ecosystem.\n\n[18:42]",
mediaUrl: "",
mediaType: "video",
scheduledAt: "2026-02-24T10:00:00",
@ -166,7 +166,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "thread",
stepNumber: 5,
content:
"\ud83c\udf44 MycoFi Earth is LIVE \ud83c\udf44\n\nAfter 2 years of building, the regenerative finance platform is here.\n\nWhat is it?\n\u2022 Token-weighted funding circles\n\u2022 Mycelial governance (no whales)\n\u2022 Composting mechanism for failed proposals\n\u2022 100% on-chain, 100% community-owned\n\n\ud83d\udc47 Full breakdown thread",
"🍄 MycoFi Earth is LIVE 🍄\n\nAfter 2 years of building, the regenerative finance platform is here.\n\nWhat is it?\n• Token-weighted funding circles\n• Mycelial governance (no whales)\n• Composting mechanism for failed proposals\n• 100% on-chain, 100% community-owned\n\n👇 Full breakdown thread",
mediaUrl: "",
mediaType: "image",
scheduledAt: "2026-02-24T10:15:00",
@ -192,7 +192,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "text",
stepNumber: 6,
content:
"Today we're launching MycoFi Earth \u2014 a regenerative finance platform modeled on mycelial networks.\n\nWhy it matters for the future of organizational design:\n\n1. Composting mechanism: Failed proposals return resources to the network\n2. Nutrient routing: Funds flow to where they're needed most\n3. No single point of failure: True decentralization\n\nFull video + docs in comments \u2193",
"Today we're launching MycoFi Earth a regenerative finance platform modeled on mycelial networks.\n\nWhy it matters for the future of organizational design:\n\n1. Composting mechanism: Failed proposals return resources to the network\n2. Nutrient routing: Funds flow to where they're needed most\n3. No single point of failure: True decentralization\n\nFull video + docs in comments ",
mediaUrl: "",
mediaType: "image",
scheduledAt: "2026-02-24T11:00:00",
@ -220,7 +220,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "reel",
stepNumber: 7,
content:
"60-second explainer: How MycoFi Earth works \ud83c\udf44\u2728\n\nVisual breakdown of the token flow from contributor \u2192 funding circle \u2192 project \u2192 compost.\n\nSet to lo-fi beats with mycelium time-lapse footage.\n\nCTA: Link in bio to join the first funding circle.",
"60-second explainer: How MycoFi Earth works 🍄✨\n\nVisual breakdown of the token flow from contributor → funding circle → project → compost.\n\nSet to lo-fi beats with mycelium time-lapse footage.\n\nCTA: Link in bio to join the first funding circle.",
mediaUrl: "",
mediaType: "video",
scheduledAt: "2026-02-25T12:00:00",
@ -246,7 +246,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "text",
stepNumber: 8,
content:
"We just launched MycoFi Earth and the response has been incredible \ud83c\udf1f\n\nThe idea is simple: what if finance worked like mycelium?\n\nMycelium doesn't hoard \u2014 it redistributes. MycoFi applies that logic to community funding.\n\nEarly access is open. Come grow with us \ud83c\udf31",
"We just launched MycoFi Earth and the response has been incredible 🌟\n\nThe idea is simple: what if finance worked like mycelium?\n\nMycelium doesn't hoard — it redistributes. MycoFi applies that logic to community funding.\n\nEarly access is open. Come grow with us 🌱",
mediaUrl: "",
mediaType: "",
scheduledAt: "2026-02-25T14:00:00",
@ -266,7 +266,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
postType: "text",
stepNumber: 9,
content:
"MycoFi Earth just went live \ud83c\udf44\n\nIt's a regenerative finance platform where funding flows like nutrients through a mycelial network.\n\nNo VCs. No whales. Just communities funding what matters.\n\nmycofi.earth",
"MycoFi Earth just went live 🍄\n\nIt's a regenerative finance platform where funding flows like nutrients through a mycelial network.\n\nNo VCs. No whales. Just communities funding what matters.\n\nmycofi.earth",
mediaUrl: "",
mediaType: "",
scheduledAt: "2026-02-25T15:00:00",
@ -286,7 +286,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
height: 160,
rotation: 0,
content:
"# \ud83c\udf44 MycoFi Earth Launch Campaign\n\n**Duration:** Feb 21\u201325, 2026 (5 days)\n**Platforms:** X, LinkedIn, Instagram, YouTube, Threads, Bluesky\n**Posts:** 9 total across 3 phases\n\n| Phase | Days | Posts |\n|-------|------|-------|\n| Pre-Launch Hype | Day -3 to -1 | 3 posts |\n| Launch Day | Day 0 | 3 posts |\n| Amplification | Day +1 | 3 posts |",
"# 🍄 MycoFi Earth Launch Campaign\n\n**Duration:** Feb 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,
rotation: 0,
content:
"### \ud83d\udce3 Phase 1: Pre-Launch Hype (Feb 21\u201323)",
"### 📣 Phase 1: Pre-Launch Hype (Feb 2123)",
},
{
id: "label-phase2",
@ -311,7 +311,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
width: COL_WIDTH * 3 + COL_GAP * 2,
height: 36,
rotation: 0,
content: "### \ud83d\ude80 Phase 2: Launch Day (Feb 24)",
content: "### 🚀 Phase 2: Launch Day (Feb 24)",
},
{
id: "label-phase3",
@ -322,7 +322,7 @@ const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
height: 36,
rotation: 0,
content:
"### \ud83d\udce1 Phase 3: Amplification (Feb 25)",
"### 📡 Phase 3: Amplification (Feb 25)",
},
// ────────────────────────────────────────────────────────────

View File

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

View File

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

View File

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

View File

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