diff --git a/lib/applet-defs.ts b/lib/applet-defs.ts index 242ad6ba..c5f14172 100644 --- a/lib/applet-defs.ts +++ b/lib/applet-defs.ts @@ -7,3 +7,18 @@ export { govApplets } from "../modules/rgov/applets"; export { flowsApplets } from "../modules/rflows/applets"; export { walletApplets } from "../modules/rwallet/applets"; +export { tasksApplets } from "../modules/rtasks/applets"; +export { timeApplets } from "../modules/rtime/applets"; +export { calApplets } from "../modules/rcal/applets"; +export { chatsApplets } from "../modules/rchats/applets"; +export { dataApplets } from "../modules/rdata/applets"; +export { docsApplets } from "../modules/rdocs/applets"; +export { notesApplets } from "../modules/rnotes/applets"; +export { photosApplets } from "../modules/rphotos/applets"; +export { mapsApplets } from "../modules/rmaps/applets"; +export { networkApplets } from "../modules/rnetwork/applets"; +export { choicesApplets } from "../modules/rchoices/applets"; +export { inboxApplets } from "../modules/rinbox/applets"; +export { socialsApplets } from "../modules/rsocials/applets"; +export { booksApplets } from "../modules/rbooks/applets"; +export { exchangeApplets } from "../modules/rexchange/applets"; diff --git a/lib/folk-applet.ts b/lib/folk-applet.ts index d56bbe86..8b06c143 100644 --- a/lib/folk-applet.ts +++ b/lib/folk-applet.ts @@ -12,7 +12,7 @@ import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; import { dataTypeColor } from "./data-types"; import type { PortDescriptor } from "./data-types"; -import type { AppletDefinition, AppletLiveData } from "../shared/applet-types"; +import type { AppletDefinition, AppletLiveData, AppletContext } from "../shared/applet-types"; // ── Applet registry (populated by modules at init) ── @@ -120,45 +120,43 @@ const styles = css` font-style: italic; } - /* Port indicators on edges */ - .port-indicator { - position: absolute; - width: 12px; - height: 12px; - border-radius: 50%; - border: 2px solid #0f172a; - cursor: crosshair; - z-index: 2; - transition: transform 0.15s; - } - - .port-indicator:hover { - transform: scale(1.4); - } - - .port-indicator.input { - left: -6px; - } - - .port-indicator.output { - right: -6px; - } - - .port-label { + /* Port chips on edges */ + .port-chip { position: absolute; + display: flex; + align-items: center; + gap: 4px; + padding: 1px 6px; + border-radius: 8px; + border: 1.5px solid; + background: var(--rs-bg-surface, #1e293b); font-size: 9px; color: var(--rs-text-muted, #94a3b8); white-space: nowrap; - pointer-events: none; + cursor: crosshair; + z-index: 2; + transform: translateY(-50%); + transition: filter 0.15s; } - .port-label.input { - left: 10px; + .port-chip:hover { + filter: brightness(1.3); } - .port-label.output { - right: 10px; - text-align: right; + .port-chip.input { + left: -2px; + } + + .port-chip.output { + right: -2px; + flex-direction: row-reverse; + } + + .chip-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; } /* Expanded mode circuit container */ @@ -250,6 +248,26 @@ export class FolkApplet extends FolkShape { return this.#instancePorts.find(p => p.name === name); } + /** Bridge FolkArrow piping → applet def's onInputReceived. */ + override setPortValue(name: string, value: unknown): void { + super.setPortValue(name, value); + + const port = this.getPort(name); + if (port?.direction !== "input") return; + + const def = getAppletDef(this.#moduleId, this.#appletId); + if (!def?.onInputReceived) return; + + const ctx: AppletContext = { + space: (this.closest("[space]") as any)?.getAttribute("space") || "", + shapeId: this.id, + emitOutput: (portName, val) => super.setPortValue(portName, val), + }; + + def.onInputReceived(name, value, ctx); + this.#renderBody(); + } + /** Update live data and re-render compact body. */ updateLiveData(snapshot: Record): void { this.#liveData = { @@ -319,57 +337,35 @@ export class FolkApplet extends FolkShape { } #renderPorts(): void { - // Remove existing port indicators - this.#wrapper.querySelectorAll(".port-indicator, .port-label").forEach(el => el.remove()); + this.#wrapper.querySelectorAll(".port-chip").forEach(el => el.remove()); - const inputs = this.getInputPorts(); - const outputs = this.getOutputPorts(); + const renderChips = (ports: PortDescriptor[], dir: "input" | "output") => { + ports.forEach((port, i) => { + const yPct = ((i + 1) / (ports.length + 1)) * 100; + const color = dataTypeColor(port.type); - // Input ports on left edge - inputs.forEach((port, i) => { - const yPct = ((i + 1) / (inputs.length + 1)) * 100; - const color = dataTypeColor(port.type); + const chip = document.createElement("div"); + chip.className = `port-chip ${dir}`; + chip.style.top = `${yPct}%`; + chip.style.borderColor = color; + chip.dataset.portName = port.name; + chip.dataset.portDir = dir; + chip.title = `${port.name} (${port.type})`; - const dot = document.createElement("div"); - dot.className = "port-indicator input"; - dot.style.top = `${yPct}%`; - dot.style.backgroundColor = color; - dot.dataset.portName = port.name; - dot.dataset.portDir = "input"; - dot.title = `${port.name} (${port.type})`; + const dot = document.createElement("span"); + dot.className = "chip-dot"; + dot.style.background = color; - const label = document.createElement("span"); - label.className = "port-label input"; - label.style.top = `${yPct}%`; - label.style.transform = "translateY(-50%)"; - label.textContent = port.name; + const label = document.createTextNode(port.name); - this.#wrapper.appendChild(dot); - this.#wrapper.appendChild(label); - }); + chip.appendChild(dot); + chip.appendChild(label); + this.#wrapper.appendChild(chip); + }); + }; - // Output ports on right edge - outputs.forEach((port, i) => { - const yPct = ((i + 1) / (outputs.length + 1)) * 100; - const color = dataTypeColor(port.type); - - const dot = document.createElement("div"); - dot.className = "port-indicator output"; - dot.style.top = `${yPct}%`; - dot.style.backgroundColor = color; - dot.dataset.portName = port.name; - dot.dataset.portDir = "output"; - dot.title = `${port.name} (${port.type})`; - - const label = document.createElement("span"); - label.className = "port-label output"; - label.style.top = `${yPct}%`; - label.style.transform = "translateY(-50%)"; - label.textContent = port.name; - - this.#wrapper.appendChild(dot); - this.#wrapper.appendChild(label); - }); + renderChips(this.getInputPorts(), "input"); + renderChips(this.getOutputPorts(), "output"); } #renderBody(): void { diff --git a/modules/rbooks/applets.ts b/modules/rbooks/applets.ts new file mode 100644 index 00000000..1f269269 --- /dev/null +++ b/modules/rbooks/applets.ts @@ -0,0 +1,42 @@ +/** + * rBooks applet definitions — Book Card. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const bookCard: AppletDefinition = { + id: "book-card", + label: "Book Card", + icon: "📚", + accentColor: "#92400e", + ports: [ + { name: "query-in", type: "string", direction: "input" }, + { name: "book-out", type: "json", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const title = (snapshot.title as string) || "Book"; + const author = (snapshot.author as string) || ""; + const progress = (snapshot.progress as number) || 0; + const rating = (snapshot.rating as number) || 0; + + return ` +
+
${title}
+ ${author ? `
${author}
` : ""} + ${rating > 0 ? `
${"★".repeat(rating)}${"☆".repeat(5 - rating)}
` : ""} +
Progress: ${progress}%
+
+
+
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "query-in" && typeof value === "string") { + ctx.emitOutput("book-out", { query: value }); + } + }, +}; + +export const booksApplets: AppletDefinition[] = [bookCard]; diff --git a/modules/rcal/applets.ts b/modules/rcal/applets.ts new file mode 100644 index 00000000..a43e824c --- /dev/null +++ b/modules/rcal/applets.ts @@ -0,0 +1,38 @@ +/** + * rCal applet definitions — Next Event. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const nextEvent: AppletDefinition = { + id: "next-event", + label: "Next Event", + icon: "📅", + accentColor: "#2563eb", + ports: [ + { name: "events-in", type: "json", direction: "input" }, + { name: "event-out", type: "json", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const title = (snapshot.title as string) || "No upcoming events"; + const time = (snapshot.time as string) || ""; + const location = (snapshot.location as string) || ""; + + return ` +
+
${title}
+ ${time ? `
🕐 ${time}
` : ""} + ${location ? `
📍 ${location}
` : ""} + ${!time && !location ? `
Connect a calendar feed
` : ""} +
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "events-in" && Array.isArray(value) && value.length > 0) { + ctx.emitOutput("event-out", value[0]); + } + }, +}; + +export const calApplets: AppletDefinition[] = [nextEvent]; diff --git a/modules/rchats/applets.ts b/modules/rchats/applets.ts new file mode 100644 index 00000000..9c046d45 --- /dev/null +++ b/modules/rchats/applets.ts @@ -0,0 +1,37 @@ +/** + * rChats applet definitions — Unread Count. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const unreadCount: AppletDefinition = { + id: "unread-count", + label: "Unread Count", + icon: "💬", + accentColor: "#0891b2", + ports: [ + { name: "channel-in", type: "string", direction: "input" }, + { name: "unread-out", type: "number", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const channel = (snapshot.channel as string) || "general"; + const unread = (snapshot.unread as number) || 0; + const badgeColor = unread > 0 ? "#ef4444" : "#334155"; + + return ` +
+
#${channel}
+
${unread}
+
unread messages
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "channel-in" && typeof value === "string") { + ctx.emitOutput("unread-out", 0); + } + }, +}; + +export const chatsApplets: AppletDefinition[] = [unreadCount]; diff --git a/modules/rchoices/applets.ts b/modules/rchoices/applets.ts new file mode 100644 index 00000000..07697807 --- /dev/null +++ b/modules/rchoices/applets.ts @@ -0,0 +1,39 @@ +/** + * rChoices applet definitions — Vote Tally. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const voteTally: AppletDefinition = { + id: "vote-tally", + label: "Vote Tally", + icon: "🗳️", + accentColor: "#7c3aed", + ports: [ + { name: "session-in", type: "json", direction: "input" }, + { name: "winner-out", type: "string", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const question = (snapshot.question as string) || "Vote"; + const totalVotes = (snapshot.totalVotes as number) || 0; + const winner = (snapshot.winner as string) || "—"; + const winPct = (snapshot.winnerPct as number) || 0; + + return ` +
+
${question}
+
${winner}
+
${winPct}% of ${totalVotes} votes
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "session-in" && value && typeof value === "object") { + const session = value as Record; + ctx.emitOutput("winner-out", (session.winner as string) || ""); + } + }, +}; + +export const choicesApplets: AppletDefinition[] = [voteTally]; diff --git a/modules/rdata/applets.ts b/modules/rdata/applets.ts new file mode 100644 index 00000000..a3cb0aba --- /dev/null +++ b/modules/rdata/applets.ts @@ -0,0 +1,42 @@ +/** + * rData applet definitions — Analytics Card. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const analyticsCard: AppletDefinition = { + id: "analytics-card", + label: "Analytics Card", + icon: "📊", + accentColor: "#6366f1", + ports: [ + { name: "metric-in", type: "number", direction: "input" }, + { name: "delta-out", type: "number", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const label = (snapshot.label as string) || "Metric"; + const value = (snapshot.value as number) || 0; + const prev = (snapshot.previous as number) || 0; + const delta = prev > 0 ? Math.round(((value - prev) / prev) * 100) : 0; + const deltaColor = delta >= 0 ? "#22c55e" : "#ef4444"; + const arrow = delta >= 0 ? "↑" : "↓"; + + return ` +
+
${label}
+
${value.toLocaleString()}
+
+ ${arrow} ${Math.abs(delta)}% vs previous +
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "metric-in" && typeof value === "number") { + ctx.emitOutput("delta-out", value); + } + }, +}; + +export const dataApplets: AppletDefinition[] = [analyticsCard]; diff --git a/modules/rdocs/applets.ts b/modules/rdocs/applets.ts new file mode 100644 index 00000000..1de982a3 --- /dev/null +++ b/modules/rdocs/applets.ts @@ -0,0 +1,39 @@ +/** + * rDocs applet definitions — Doc Summary. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const docSummary: AppletDefinition = { + id: "doc-summary", + label: "Doc Summary", + icon: "📄", + accentColor: "#d97706", + ports: [ + { name: "doc-in", type: "json", direction: "input" }, + { name: "text-out", type: "text", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const title = (snapshot.title as string) || "Untitled"; + const wordCount = (snapshot.wordCount as number) || 0; + const lastEdit = (snapshot.lastEdit as string) || ""; + const preview = (snapshot.preview as string) || "No content"; + + return ` +
+
${title}
+
${wordCount} words${lastEdit ? ` · ${lastEdit}` : ""}
+
${preview}
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "doc-in" && value && typeof value === "object") { + const doc = value as Record; + ctx.emitOutput("text-out", (doc.content as string) || (doc.preview as string) || ""); + } + }, +}; + +export const docsApplets: AppletDefinition[] = [docSummary]; diff --git a/modules/rexchange/applets.ts b/modules/rexchange/applets.ts new file mode 100644 index 00000000..9e70d8f3 --- /dev/null +++ b/modules/rexchange/applets.ts @@ -0,0 +1,75 @@ +/** + * rExchange applet definitions — Rate Card + Trade Status. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const rateCard: AppletDefinition = { + id: "rate-card", + label: "Rate Card", + icon: "💱", + accentColor: "#047857", + ports: [ + { name: "pair-in", type: "string", direction: "input" }, + { name: "rate-out", type: "number", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const pair = (snapshot.pair as string) || "—/—"; + const rate = (snapshot.rate as number) || 0; + const change24h = (snapshot.change24h as number) || 0; + const changeColor = change24h >= 0 ? "#22c55e" : "#ef4444"; + const arrow = change24h >= 0 ? "▲" : "▼"; + + return ` +
+
${pair}
+
${rate.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}
+
${arrow} ${Math.abs(change24h).toFixed(2)}%
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "pair-in" && typeof value === "string") { + ctx.emitOutput("rate-out", 0); + } + }, +}; + +const tradeStatus: AppletDefinition = { + id: "trade-status", + label: "Trade Status", + icon: "🔄", + accentColor: "#047857", + ports: [ + { name: "trade-in", type: "json", direction: "input" }, + { name: "status-out", type: "string", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const type = (snapshot.type as string) || "trade"; + const amount = (snapshot.amount as number) || 0; + const asset = (snapshot.asset as string) || "—"; + const status = (snapshot.status as string) || "pending"; + const statusColor = status === "filled" ? "#22c55e" : status === "cancelled" ? "#ef4444" : "#f59e0b"; + + return ` +
+
+ ${type} + ${status} +
+
${amount.toLocaleString()}
+
${asset}
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "trade-in" && value && typeof value === "object") { + const trade = value as Record; + ctx.emitOutput("status-out", (trade.status as string) || "pending"); + } + }, +}; + +export const exchangeApplets: AppletDefinition[] = [rateCard, tradeStatus]; diff --git a/modules/rinbox/applets.ts b/modules/rinbox/applets.ts new file mode 100644 index 00000000..fa859259 --- /dev/null +++ b/modules/rinbox/applets.ts @@ -0,0 +1,43 @@ +/** + * rInbox applet definitions — Thread Feed. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const threadFeed: AppletDefinition = { + id: "thread-feed", + label: "Thread Feed", + icon: "📬", + accentColor: "#0e7490", + ports: [ + { name: "mailbox-in", type: "string", direction: "input" }, + { name: "count-out", type: "number", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const total = (snapshot.total as number) || 0; + const unread = (snapshot.unread as number) || 0; + const latest = (snapshot.latestSubject as string) || "No messages"; + + return ` +
+
+ Total + ${total} +
+
+ Unread + ${unread} +
+
${latest}
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "mailbox-in" && typeof value === "string") { + ctx.emitOutput("count-out", 0); + } + }, +}; + +export const inboxApplets: AppletDefinition[] = [threadFeed]; diff --git a/modules/rmaps/applets.ts b/modules/rmaps/applets.ts new file mode 100644 index 00000000..da06c016 --- /dev/null +++ b/modules/rmaps/applets.ts @@ -0,0 +1,83 @@ +/** + * rMaps applet definitions — Location Pin + Route Summary. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const locationPin: AppletDefinition = { + id: "location-pin", + label: "Location Pin", + icon: "📍", + accentColor: "#1d4ed8", + ports: [ + { name: "location-in", type: "json", direction: "input" }, + { name: "coords-out", type: "json", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const label = (snapshot.label as string) || "Location"; + const lat = (snapshot.lat as number) || 0; + const lng = (snapshot.lng as number) || 0; + const hasCoords = lat !== 0 || lng !== 0; + + return ` +
+
📍
+
${label}
+ ${hasCoords + ? `
${lat.toFixed(4)}, ${lng.toFixed(4)}
` + : `
No coordinates
` + } +
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "location-in" && value && typeof value === "object") { + const loc = value as Record; + ctx.emitOutput("coords-out", { lat: loc.lat, lng: loc.lng }); + } + }, +}; + +const routeSummary: AppletDefinition = { + id: "route-summary", + label: "Route Summary", + icon: "🗺️", + accentColor: "#1d4ed8", + ports: [ + { name: "route-in", type: "json", direction: "input" }, + { name: "distance-out", type: "number", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const from = (snapshot.from as string) || "Start"; + const to = (snapshot.to as string) || "End"; + const distance = (snapshot.distance as string) || "—"; + const duration = (snapshot.duration as string) || "—"; + + return ` +
+
+ From + ${from} +
+
+ To + ${to} +
+
+ ${distance} + ${duration} +
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "route-in" && value && typeof value === "object") { + const route = value as Record; + ctx.emitOutput("distance-out", Number(route.distanceKm) || 0); + } + }, +}; + +export const mapsApplets: AppletDefinition[] = [locationPin, routeSummary]; diff --git a/modules/rnetwork/applets.ts b/modules/rnetwork/applets.ts new file mode 100644 index 00000000..9d4fa2b9 --- /dev/null +++ b/modules/rnetwork/applets.ts @@ -0,0 +1,40 @@ +/** + * rNetwork applet definitions — Contact Card. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const contactCard: AppletDefinition = { + id: "contact-card", + label: "Contact Card", + icon: "👤", + accentColor: "#4f46e5", + ports: [ + { name: "did-in", type: "string", direction: "input" }, + { name: "contact-out", type: "json", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const name = (snapshot.name as string) || "Unknown"; + const did = (snapshot.did as string) || ""; + const shortDid = did ? `${did.slice(0, 16)}…` : "No DID"; + const trustScore = (snapshot.trustScore as number) || 0; + const trustColor = trustScore >= 0.7 ? "#22c55e" : trustScore >= 0.4 ? "#f59e0b" : "#94a3b8"; + + return ` +
+
👤
+
${name}
+
${shortDid}
+
Trust: ${Math.round(trustScore * 100)}%
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "did-in" && typeof value === "string") { + ctx.emitOutput("contact-out", { did: value, name: "", trustScore: 0 }); + } + }, +}; + +export const networkApplets: AppletDefinition[] = [contactCard]; diff --git a/modules/rnotes/applets.ts b/modules/rnotes/applets.ts new file mode 100644 index 00000000..5caf5650 --- /dev/null +++ b/modules/rnotes/applets.ts @@ -0,0 +1,40 @@ +/** + * rNotes applet definitions — Vault Note. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const vaultNote: AppletDefinition = { + id: "vault-note", + label: "Vault Note", + icon: "🗒️", + accentColor: "#065f46", + ports: [ + { name: "note-in", type: "json", direction: "input" }, + { name: "content-out", type: "text", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const title = (snapshot.title as string) || "Note"; + const vault = (snapshot.vault as string) || ""; + const tags = (snapshot.tags as string[]) || []; + const preview = (snapshot.preview as string) || "Empty note"; + + return ` +
+
${title}
+ ${vault ? `
📁 ${vault}
` : ""} + ${tags.length > 0 ? `
${tags.map(t => `#${t}`).join(" ")}
` : ""} +
${preview}
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "note-in" && value && typeof value === "object") { + const note = value as Record; + ctx.emitOutput("content-out", (note.content as string) || ""); + } + }, +}; + +export const notesApplets: AppletDefinition[] = [vaultNote]; diff --git a/modules/rphotos/applets.ts b/modules/rphotos/applets.ts new file mode 100644 index 00000000..01d356c9 --- /dev/null +++ b/modules/rphotos/applets.ts @@ -0,0 +1,40 @@ +/** + * rPhotos applet definitions — Album Card. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const albumCard: AppletDefinition = { + id: "album-card", + label: "Album Card", + icon: "🖼️", + accentColor: "#be185d", + ports: [ + { name: "album-in", type: "string", direction: "input" }, + { name: "image-out", type: "image-url", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const name = (snapshot.name as string) || "Album"; + const count = (snapshot.count as number) || 0; + const thumb = (snapshot.thumbnail as string) || ""; + + return ` +
+ ${thumb + ? `
` + : `
🖼️
` + } +
${name}
+
${count} photos
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "album-in" && typeof value === "string") { + ctx.emitOutput("image-out", ""); + } + }, +}; + +export const photosApplets: AppletDefinition[] = [albumCard]; diff --git a/modules/rsocials/applets.ts b/modules/rsocials/applets.ts new file mode 100644 index 00000000..23d6adc0 --- /dev/null +++ b/modules/rsocials/applets.ts @@ -0,0 +1,45 @@ +/** + * rSocials applet definitions — Post Draft. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const postDraft: AppletDefinition = { + id: "post-draft", + label: "Post Draft", + icon: "✏️", + accentColor: "#db2777", + ports: [ + { name: "content-in", type: "text", direction: "input" }, + { name: "post-out", type: "json", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const platform = (snapshot.platform as string) || "social"; + const content = (snapshot.content as string) || ""; + const charCount = content.length; + const maxChars = (snapshot.maxChars as number) || 280; + const pct = Math.min(100, Math.round((charCount / maxChars) * 100)); + const countColor = pct > 90 ? "#ef4444" : pct > 70 ? "#f59e0b" : "#94a3b8"; + + return ` +
+
${platform}
+
${content || "Empty draft"}
+
+
+
+
+ ${charCount}/${maxChars} +
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "content-in" && typeof value === "string") { + ctx.emitOutput("post-out", { content: value, platform: "social", timestamp: Date.now() }); + } + }, +}; + +export const socialsApplets: AppletDefinition[] = [postDraft]; diff --git a/modules/rtasks/applets.ts b/modules/rtasks/applets.ts new file mode 100644 index 00000000..fd61a2c5 --- /dev/null +++ b/modules/rtasks/applets.ts @@ -0,0 +1,70 @@ +/** + * rTasks applet definitions — Task Counter + Due Today. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const taskCounter: AppletDefinition = { + id: "task-counter", + label: "Task Counter", + icon: "📋", + accentColor: "#0f766e", + ports: [ + { name: "board-in", type: "json", direction: "input" }, + { name: "count-out", type: "number", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const total = (snapshot.total as number) || 0; + const done = (snapshot.done as number) || 0; + const pct = total > 0 ? Math.round((done / total) * 100) : 0; + + return ` +
+
${done}/${total}
+
tasks complete
+
+
+
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "board-in" && value && typeof value === "object") { + const board = value as Record; + ctx.emitOutput("count-out", Number(board.total) || 0); + } + }, +}; + +const dueToday: AppletDefinition = { + id: "due-today", + label: "Due Today", + icon: "⏰", + accentColor: "#0f766e", + ports: [ + { name: "board-in", type: "json", direction: "input" }, + { name: "tasks-out", type: "json", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const count = (snapshot.dueCount as number) || 0; + const urgent = (snapshot.urgentCount as number) || 0; + const urgColor = urgent > 0 ? "#ef4444" : "#22c55e"; + + return ` +
+
${count}
+
due today
+
${urgent} urgent
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "board-in" && value && typeof value === "object") { + ctx.emitOutput("tasks-out", value); + } + }, +}; + +export const tasksApplets: AppletDefinition[] = [taskCounter, dueToday]; diff --git a/modules/rtime/applets.ts b/modules/rtime/applets.ts new file mode 100644 index 00000000..c06b6535 --- /dev/null +++ b/modules/rtime/applets.ts @@ -0,0 +1,42 @@ +/** + * rTime applet definitions — Commitment Meter. + */ + +import type { AppletDefinition, AppletLiveData } from "../../shared/applet-types"; + +const commitmentMeter: AppletDefinition = { + id: "commitment-meter", + label: "Commitment Meter", + icon: "⏳", + accentColor: "#7c3aed", + ports: [ + { name: "pool-in", type: "json", direction: "input" }, + { name: "committed-out", type: "number", direction: "output" }, + ], + renderCompact(data: AppletLiveData): string { + const { snapshot } = data; + const committed = (snapshot.committed as number) || 0; + const capacity = (snapshot.capacity as number) || 1; + const pct = Math.min(100, Math.round((committed / capacity) * 100)); + const label = (snapshot.poolName as string) || "Pool"; + + return ` +
+
${label}
+
${pct}%
+
${committed}/${capacity} hrs
+
+
+
+
+ `; + }, + onInputReceived(portName, value, ctx) { + if (portName === "pool-in" && value && typeof value === "object") { + const pool = value as Record; + ctx.emitOutput("committed-out", Number(pool.committed) || 0); + } + }, +}; + +export const timeApplets: AppletDefinition[] = [commitmentMeter]; diff --git a/website/canvas.html b/website/canvas.html index cde7b9ee..a3f47b29 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -2150,6 +2150,17 @@ + + +
+ +
+
Applets
+
+
Templates
+
No templates saved
+
+
@@ -2518,6 +2529,7 @@ FolkHolonExplorer, FolkApplet, registerAppletDef, + listAppletDefs, AppletTemplateManager, CommunitySync, PresenceManager, @@ -2543,7 +2555,13 @@ import { RStackTabBar } from "@shared/components/rstack-tab-bar"; import { RStackMi } from "@shared/components/rstack-mi"; - import { govApplets, flowsApplets, walletApplets } from "@lib/applet-defs"; + import { + govApplets, flowsApplets, walletApplets, + tasksApplets, timeApplets, calApplets, chatsApplets, + dataApplets, docsApplets, notesApplets, photosApplets, + mapsApplets, networkApplets, choicesApplets, inboxApplets, + socialsApplets, booksApplets, exchangeApplets, + } from "@lib/applet-defs"; import { RStackHistoryPanel } from "@shared/components/rstack-history-panel"; import { RStackCommentBell } from "@shared/components/rstack-comment-bell"; import { rspaceNavUrl } from "@shared/url-helpers"; @@ -2844,6 +2862,83 @@ for (const def of govApplets) registerAppletDef("rgov", def); for (const def of flowsApplets) registerAppletDef("rflows", def); for (const def of walletApplets) registerAppletDef("rwallet", def); + for (const def of tasksApplets) registerAppletDef("rtasks", def); + for (const def of timeApplets) registerAppletDef("rtime", def); + for (const def of calApplets) registerAppletDef("rcal", def); + for (const def of chatsApplets) registerAppletDef("rchats", def); + for (const def of dataApplets) registerAppletDef("rdata", def); + for (const def of docsApplets) registerAppletDef("rdocs", def); + for (const def of notesApplets) registerAppletDef("rnotes", def); + for (const def of photosApplets) registerAppletDef("rphotos", def); + for (const def of mapsApplets) registerAppletDef("rmaps", def); + for (const def of networkApplets) registerAppletDef("rnetwork", def); + for (const def of choicesApplets) registerAppletDef("rchoices", def); + for (const def of inboxApplets) registerAppletDef("rinbox", def); + for (const def of socialsApplets) registerAppletDef("rsocials", def); + for (const def of booksApplets) registerAppletDef("rbooks", def); + for (const def of exchangeApplets) registerAppletDef("rexchange", def); + + // Build applet palette in toolbar + function buildAppletPalette() { + const list = document.getElementById("applet-palette-list"); + if (!list) return; + list.innerHTML = ""; + const defs = listAppletDefs(); + // Group by module + const byModule = new Map(); + for (const { moduleId, def } of defs) { + if (!byModule.has(moduleId)) byModule.set(moduleId, []); + byModule.get(moduleId).push(def); + } + for (const [modId, modDefs] of byModule) { + for (const def of modDefs) { + const btn = document.createElement("button"); + btn.title = `${def.label} (${modId})`; + btn.textContent = `${def.icon} ${def.label}`; + btn.style.borderLeft = `3px solid ${def.accentColor}`; + btn.addEventListener("click", () => { + setPendingTool("folk-applet", { moduleId: modId, appletId: def.id }); + }); + list.appendChild(btn); + } + } + } + buildAppletPalette(); + + // Build template palette (refresh on group open) + function buildTemplatePalette() { + const list = document.getElementById("applet-template-list"); + if (!list) return; + if (typeof AppletTemplateManager === "undefined") return; + const templates = AppletTemplateManager.listTemplates?.(); + if (!templates || templates.length === 0) { + list.innerHTML = 'No templates saved'; + return; + } + list.innerHTML = ""; + for (const tmpl of templates) { + const btn = document.createElement("button"); + btn.title = tmpl.description || tmpl.name; + btn.textContent = `${tmpl.icon || "📐"} ${tmpl.name}`; + btn.style.borderLeft = `3px solid ${tmpl.color || "#475569"}`; + btn.addEventListener("click", () => { + const center = getViewportCenter(); + AppletTemplateManager.instantiateTemplate?.(tmpl.id, center.x, center.y); + }); + list.appendChild(btn); + } + } + + // Refresh template list when applets group opens + const appletsGroup = document.getElementById("applets-group"); + if (appletsGroup) { + const toggle = appletsGroup.querySelector(".toolbar-group-toggle"); + if (toggle) { + toggle.addEventListener("click", () => { + setTimeout(buildTemplatePalette, 0); + }); + } + } // Zoom and pan state — declared early to avoid TDZ errors // (event handlers reference these before awaits yield execution) @@ -4093,6 +4188,7 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest "folk-holon-browser": { width: 400, height: 450 }, "folk-holon-explorer": { width: 580, height: 540 }, "folk-transaction-builder": { width: 420, height: 520 }, + "folk-applet": { width: 300, height: 200 }, }; // Get the center of the current viewport in canvas coordinates