From a6008a4f2dd1f8f7249a5d9485241253411c7e34 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 3 Mar 2026 19:13:14 -0800 Subject: [PATCH] =?UTF-8?q?refactor:=20complete=20rfunds=20=E2=86=92=20rfl?= =?UTF-8?q?ows=20rename=20across=20configs=20and=20references?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update docker-compose, vite config, Traefik labels, module imports, and all cross-module references to use the new rflows naming. Co-Authored-By: Claude Opus 4.6 --- docker-compose.standalone.yml | 14 +- docker-compose.yml | 8 +- lib/folk-rapp.ts | 4 +- modules/rcal/mod.ts | 4 +- modules/rcart/landing.ts | 4 +- .../rdata/components/folk-analytics-view.ts | 2 +- modules/rdata/mod.ts | 2 +- modules/rflows/components/flows-demo.ts | 2 +- modules/rflows/components/flows.css | 306 +++++++------ modules/rflows/components/folk-flow-river.ts | 8 +- modules/rflows/components/folk-flows-app.ts | 425 ++++++++++-------- modules/rflows/landing.ts | 16 +- modules/rflows/lib/map-flow.ts | 2 +- modules/rflows/lib/presets.ts | 2 +- modules/rflows/lib/simulation.ts | 2 +- modules/rflows/lib/types.ts | 2 +- modules/rflows/local-first-client.ts | 42 +- modules/rflows/mod.ts | 74 +-- modules/rflows/schemas.ts | 22 +- modules/rnotes/components/folk-notes-app.ts | 2 +- modules/rnotes/mod.ts | 4 +- modules/rtrips/mod.ts | 4 +- modules/rwallet/landing.ts | 2 +- server/seed-demo.ts | 4 +- server/seed-template.ts | 4 +- shared/components/rstack-app-switcher.ts | 4 +- shared/components/rstack-tab-bar.ts | 4 +- vite.config.ts | 46 +- website/canvas.html | 4 +- website/index.html | 8 +- website/public/landing.html | 6 +- 31 files changed, 560 insertions(+), 473 deletions(-) diff --git a/docker-compose.standalone.yml b/docker-compose.standalone.yml index 20e59b8..758d366 100644 --- a/docker-compose.standalone.yml +++ b/docker-compose.standalone.yml @@ -109,11 +109,11 @@ services: traefik.http.routers.rchoices-sa.entrypoints: web traefik.http.services.rchoices-sa.loadbalancer.server.port: "3000" - # ── rFunds ── - rfunds-standalone: + # ── rFlows ── + rflows-standalone: <<: *standalone-base - container_name: rfunds-standalone - command: ["bun", "run", "modules/rfunds/standalone.ts"] + container_name: rflows-standalone + command: ["bun", "run", "modules/rflows/standalone.ts"] environment: <<: *base-env FLOW_SERVICE_URL: http://payment-flow:3010 @@ -125,9 +125,9 @@ services: - payment-network labels: <<: *traefik-enabled - traefik.http.routers.rfunds-sa.rule: Host(`rfunds.online`) - traefik.http.routers.rfunds-sa.entrypoints: web - traefik.http.services.rfunds-sa.loadbalancer.server.port: "3000" + traefik.http.routers.rflows-sa.rule: Host(`rflows.online`) + traefik.http.routers.rflows-sa.entrypoints: web + traefik.http.services.rflows-sa.loadbalancer.server.port: "3000" # ── rFiles ── rfiles-standalone: diff --git a/docker-compose.yml b/docker-compose.yml index 0d9b38a..e3d6a08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,10 +75,10 @@ services: - "traefik.http.routers.rspace-rchoices.entrypoints=web" - "traefik.http.routers.rspace-rchoices.priority=120" - "traefik.http.routers.rspace-rchoices.service=rspace-online" - - "traefik.http.routers.rspace-rfunds.rule=Host(`rfunds.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rfunds.online`)" - - "traefik.http.routers.rspace-rfunds.entrypoints=web" - - "traefik.http.routers.rspace-rfunds.priority=120" - - "traefik.http.routers.rspace-rfunds.service=rspace-online" + - "traefik.http.routers.rspace-rflows.rule=Host(`rflows.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rflows.online`)" + - "traefik.http.routers.rspace-rflows.entrypoints=web" + - "traefik.http.routers.rspace-rflows.priority=120" + - "traefik.http.routers.rspace-rflows.service=rspace-online" - "traefik.http.routers.rspace-rforum.rule=Host(`rforum.online`) || HostRegexp(`{sub:[a-z0-9-]+}.rforum.online`)" - "traefik.http.routers.rspace-rforum.entrypoints=web" - "traefik.http.routers.rspace-rforum.priority=120" diff --git a/lib/folk-rapp.ts b/lib/folk-rapp.ts index 77fd71a..bbd00e6 100644 --- a/lib/folk-rapp.ts +++ b/lib/folk-rapp.ts @@ -26,7 +26,7 @@ const MODULE_META: Record Widge }; }, }, - rfunds: { + rflows: { path: "/api/flows", transform: (data) => { const flows = Array.isArray(data) ? data : []; diff --git a/modules/rcal/mod.ts b/modules/rcal/mod.ts index 0bd0055..a7c8285 100644 --- a/modules/rcal/mod.ts +++ b/modules/rcal/mod.ts @@ -191,11 +191,11 @@ function seedDemoIfEmpty(space: string) { sourceId: sprintsId, allDay: true, }, { - title: "rFunds Budget Review", + title: "rFlows Budget Review", desc: "Quarterly review of treasury flows, enoughness thresholds, and overflow routing.", start: daysFromNow(6, 15, 0), end: daysFromNow(6, 17, 0), sourceId: communityId, isVirtual: true, - virtualUrl: "https://meet.jit.si/rfunds-review", virtualPlatform: "Jitsi", + virtualUrl: "https://meet.jit.si/rflows-review", virtualPlatform: "Jitsi", }, { title: "Cosmolocal Design Sprint", diff --git a/modules/rcart/landing.ts b/modules/rcart/landing.ts index 8242ae8..f669534 100644 --- a/modules/rcart/landing.ts +++ b/modules/rcart/landing.ts @@ -41,7 +41,7 @@ export function renderLanding(): string {
💰

Revenue Splits

-

Creator, community, and provider shares calculated automatically via rFunds. Transparent by default.

+

Creator, community, and provider shares calculated automatically via rFlows. Transparent by default.

@@ -82,7 +82,7 @@ export function renderLanding(): string {

  • Provider matching — automatic routing by capability, location, and cost
  • -
  • Revenue splits — creator, community, and provider shares via rFunds
  • +
  • Revenue splits — creator, community, and provider shares via rFlows
  • Order tracking — real-time status from accepted to delivered
  • Volume pricing — automatic tier detection from pooled orders
diff --git a/modules/rdata/components/folk-analytics-view.ts b/modules/rdata/components/folk-analytics-view.ts index c4afc76..723da48 100644 --- a/modules/rdata/components/folk-analytics-view.ts +++ b/modules/rdata/components/folk-analytics-view.ts @@ -29,7 +29,7 @@ class FolkAnalyticsView extends HTMLElement { cookiesSet: 0, scriptSize: "~2KB", selfHosted: true, - apps: ["rSpace", "rBooks", "rCart", "rFunds", "rVote", "rWork", "rCal", "rTrips", "rPubs", "rForum", "rInbox", "rMaps", "rTube", "rChoices", "rWallet", "rData", "rNotes"], + apps: ["rSpace", "rBooks", "rCart", "rFlows", "rVote", "rWork", "rCal", "rTrips", "rPubs", "rForum", "rInbox", "rMaps", "rTube", "rChoices", "rWallet", "rData", "rNotes"], dashboardUrl: "https://analytics.rspace.online", }; this.render(); diff --git a/modules/rdata/mod.ts b/modules/rdata/mod.ts index 3595fdd..da12662 100644 --- a/modules/rdata/mod.ts +++ b/modules/rdata/mod.ts @@ -17,7 +17,7 @@ const UMAMI_URL = process.env.UMAMI_URL || "https://analytics.rspace.online"; const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID || "292f6ac6-79f8-497b-ba6a-7a51e3b87b9f"; const TRACKED_APPS = [ - "rSpace", "rNotes", "rVote", "rFunds", "rCart", "rWallet", + "rSpace", "rNotes", "rVote", "rFlows", "rCart", "rWallet", "rPubs", "rChoices", "rCal", "rForum", "rInbox", "rFiles", "rTrips", "rTube", "rWork", "rNetwork", "rData", ]; diff --git a/modules/rflows/components/flows-demo.ts b/modules/rflows/components/flows-demo.ts index 466c25a..e6459a3 100644 --- a/modules/rflows/components/flows-demo.ts +++ b/modules/rflows/components/flows-demo.ts @@ -1,5 +1,5 @@ /** - * rFunds demo — client-side WebSocket controller. + * rFlows demo — client-side WebSocket controller. * * Connects via DemoSync, extracts expenses and budget from shapes, * renders/updates budget overview, expense list, balances, settlements, diff --git a/modules/rflows/components/flows.css b/modules/rflows/components/flows.css index 1c6db8d..7a90e06 100644 --- a/modules/rflows/components/flows.css +++ b/modules/rflows/components/flows.css @@ -1,178 +1,191 @@ -/* ── Funds module theme ───────────────────────────────── */ +/* ── Flows module theme ───────────────────────────────── */ + +/* ── Base ────────────────────────────────────────────── */ +.flows-landing, .flows-detail { + font-family: system-ui, -apple-system, sans-serif; +} + +/* Thin scrollbars (rApp convention) */ +.flows-detail ::-webkit-scrollbar, +.flows-landing ::-webkit-scrollbar { width: 6px; height: 6px; } +.flows-detail ::-webkit-scrollbar-track, +.flows-landing ::-webkit-scrollbar-track { background: transparent; } +.flows-detail ::-webkit-scrollbar-thumb, +.flows-landing ::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; } /* ── Shared utility classes ──────────────────────────── */ -.funds-loading { text-align: center; color: #64748b; padding: 48px 16px; font-size: 14px; } -.funds-error { text-align: center; color: #ef4444; padding: 20px 16px; font-size: 14px; } +.flows-loading { text-align: center; color: #64748b; padding: 48px 16px; font-size: 14px; } +.flows-error { text-align: center; color: #ef4444; padding: 20px 16px; font-size: 14px; } /* ── Landing page ────────────────────────────────────── */ -.funds-landing { max-width: 960px; margin: 0 auto; padding: 24px 20px 64px; } +.flows-landing { max-width: 960px; margin: 0 auto; padding: 24px 20px 64px; } /* Features grid */ -.funds-features { margin-bottom: 48px; } -.funds-features__grid { +.flows-features { margin-bottom: 48px; } +.flows-features__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 12px; } -.funds-features__card { - background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 20px; +.flows-features__card { + background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; transition: border-color 0.2s; } -.funds-features__card:hover { border-color: #475569; } -.funds-features__icon { font-size: 24px; margin-bottom: 8px; } -.funds-features__card h3 { font-size: 14px; font-weight: 600; color: #e2e8f0; margin: 0 0 6px; } -.funds-features__card p { font-size: 12px; color: #94a3b8; line-height: 1.6; margin: 0; } +.flows-features__card:hover { border-color: #475569; } +.flows-features__icon { font-size: 24px; margin-bottom: 8px; } +.flows-features__card h3 { font-size: 14px; font-weight: 600; color: #e2e8f0; margin: 0 0 6px; } +.flows-features__card p { font-size: 12px; color: #94a3b8; line-height: 1.6; margin: 0; } /* Flow list */ -.funds-flows { margin-bottom: 48px; } -.funds-flows__header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 16px; gap: 12px; flex-wrap: wrap; } -.funds-flows__heading { font-size: 18px; font-weight: 600; color: #e2e8f0; margin: 0; } -.funds-flows__user { font-size: 12px; color: #64748b; } -.funds-flows__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; } -.funds-flows__empty { +.flows-flows { margin-bottom: 48px; } +.flows-flows__header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 16px; gap: 12px; flex-wrap: wrap; } +.flows-flows__heading { font-size: 18px; font-weight: 600; color: #e2e8f0; margin: 0; } +.flows-flows__user { font-size: 12px; color: #64748b; } +.flows-flows__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; } +.flows-flows__empty { text-align: center; color: #64748b; padding: 32px 16px; font-size: 14px; - background: #1e293b; border: 1px solid #334155; border-radius: 10px; + background: #1e293b; border: 1px solid #334155; border-radius: 8px; } -.funds-flows__empty a { color: #6366f1; text-decoration: none; } -.funds-flows__empty a:hover { text-decoration: underline; } +.flows-flows__empty a { color: #6366f1; text-decoration: none; } +.flows-flows__empty a:hover { text-decoration: underline; } -.funds-flow-card { +.flows-flow-card { display: block; text-decoration: none; - background: #1e293b; border: 1px solid #334155; border-radius: 10px; + background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 16px; cursor: pointer; transition: border-color 0.2s, transform 0.15s; } -.funds-flow-card:hover { border-color: #6366f1; transform: translateY(-1px); } -.funds-flow-card__name { font-size: 15px; font-weight: 600; color: #e2e8f0; margin-bottom: 4px; } -.funds-flow-card__value { font-size: 20px; font-weight: 700; color: #0ea5e9; margin-bottom: 4px; } -.funds-flow-card__meta { font-size: 12px; color: #64748b; } +.flows-flow-card:hover { border-color: #6366f1; transform: translateY(-1px); } +.flows-flow-card__name { font-size: 15px; font-weight: 600; color: #e2e8f0; margin-bottom: 4px; } +.flows-flow-card__value { font-size: 20px; font-weight: 700; color: #0ea5e9; margin-bottom: 4px; } +.flows-flow-card__meta { font-size: 12px; color: #64748b; } /* About / how-it-works section */ -.funds-about { margin-bottom: 48px; } -.funds-about__heading { font-size: 18px; font-weight: 600; color: #e2e8f0; margin: 0 0 20px; } +.flows-about { margin-bottom: 48px; } +.flows-about__heading { font-size: 18px; font-weight: 600; color: #e2e8f0; margin: 0 0 20px; } /* Steps layout (replaces the old card grid for "how it works") */ -.funds-about__steps { display: flex; flex-direction: column; gap: 16px; } -.funds-about__step { +.flows-about__steps { display: flex; flex-direction: column; gap: 16px; } +.flows-about__step { display: flex; gap: 16px; align-items: flex-start; - background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 20px; + background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; } -.funds-about__step-num { +.flows-about__step-num { width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0; background: #4f46e5; color: #fff; font-weight: 700; font-size: 14px; display: flex; align-items: center; justify-content: center; } -.funds-about__step h3 { font-size: 14px; font-weight: 600; color: #e2e8f0; margin: 0 0 4px; } -.funds-about__step p { font-size: 13px; color: #94a3b8; line-height: 1.6; margin: 0; } +.flows-about__step h3 { font-size: 14px; font-weight: 600; color: #e2e8f0; margin: 0 0 4px; } +.flows-about__step p { font-size: 13px; color: #94a3b8; line-height: 1.6; margin: 0; } /* Legacy about grid (kept for compat) */ -.funds-about__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } -.funds-about__card { - background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 20px; +.flows-about__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } +.flows-about__card { + background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; } -.funds-about__icon { font-size: 28px; margin-bottom: 8px; } -.funds-about__card h3 { font-size: 15px; font-weight: 600; color: #e2e8f0; margin: 0 0 8px; } -.funds-about__card p { font-size: 13px; color: #94a3b8; line-height: 1.6; margin: 0; } +.flows-about__icon { font-size: 28px; margin-bottom: 8px; } +.flows-about__card h3 { font-size: 15px; font-weight: 600; color: #e2e8f0; margin: 0 0 8px; } +.flows-about__card p { font-size: 13px; color: #94a3b8; line-height: 1.6; margin: 0; } /* ── Detail view ─────────────────────────────────────── */ -.funds-detail { max-width: 1100px; margin: 0 auto; padding: 16px 20px 64px; } +.flows-detail { max-width: 1100px; margin: 0 auto; padding: 16px 20px 64px; } /* ── Tabs ────────────────────────────────────────────── */ -.funds-tabs { +.flows-tabs { display: flex; gap: 4px; border-bottom: 1px solid #1e293b; margin-bottom: 20px; } -.funds-tab { +.flows-tab { padding: 8px 18px; border: none; border-bottom: 2px solid transparent; background: transparent; color: #64748b; font-size: 13px; font-weight: 500; cursor: pointer; transition: color 0.2s, border-color 0.2s; } -.funds-tab:hover { color: #e2e8f0; } -.funds-tab--active { color: #e2e8f0; border-bottom-color: #6366f1; } +.flows-tab:hover { color: #e2e8f0; } +.flows-tab--active { color: #e2e8f0; border-bottom-color: #6366f1; } -.funds-tab-content { min-height: 300px; } +.flows-tab-content { min-height: 300px; } /* ── Table tab — card grid ───────────────────────────── */ -.funds-table { } -.funds-section { margin-bottom: 28px; } -.funds-section__title { font-size: 14px; font-weight: 600; color: #94a3b8; margin: 0 0 12px; text-transform: uppercase; letter-spacing: 0.05em; } -.funds-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; } +.flows-table { } +.flows-section { margin-bottom: 28px; } +.flows-section__title { font-size: 14px; font-weight: 600; color: #94a3b8; margin: 0 0 12px; text-transform: uppercase; letter-spacing: 0.05em; } +.flows-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; } -.funds-card { - background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 16px; +.flows-card { + background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 16px; } -.funds-card__header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } -.funds-card__icon { font-size: 18px; } -.funds-card__label { font-size: 14px; font-weight: 600; color: #e2e8f0; flex: 1; } -.funds-card__type { font-size: 11px; color: #64748b; text-transform: uppercase; } -.funds-card__status { font-size: 11px; font-weight: 600; text-transform: capitalize; } -.funds-card__desc { font-size: 12px; color: #94a3b8; margin-bottom: 10px; line-height: 1.5; } +.flows-card__header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } +.flows-card__icon { font-size: 18px; } +.flows-card__label { font-size: 14px; font-weight: 600; color: #e2e8f0; flex: 1; } +.flows-card__type { font-size: 11px; color: #64748b; text-transform: uppercase; } +.flows-card__status { font-size: 11px; font-weight: 600; text-transform: capitalize; } +.flows-card__desc { font-size: 12px; color: #94a3b8; margin-bottom: 10px; line-height: 1.5; } -.funds-card__stat { margin-bottom: 10px; } -.funds-card__stat-value { font-size: 18px; font-weight: 700; color: #e2e8f0; } -.funds-card__stat-label { font-size: 12px; color: #64748b; margin-left: 4px; } -.funds-card__stats { display: flex; justify-content: space-between; margin-bottom: 8px; } +.flows-card__stat { margin-bottom: 10px; } +.flows-card__stat-value { font-size: 18px; font-weight: 700; color: #e2e8f0; } +.flows-card__stat-label { font-size: 12px; color: #64748b; margin-left: 4px; } +.flows-card__stats { display: flex; justify-content: space-between; margin-bottom: 8px; } /* Progress bar */ -.funds-card__bar-container { +.flows-card__bar-container { position: relative; height: 6px; background: #334155; border-radius: 3px; margin-bottom: 10px; overflow: visible; } -.funds-card__bar { +.flows-card__bar { height: 100%; border-radius: 3px; background: #0ea5e9; transition: width 0.3s ease; } -.funds-card__bar--outcome { opacity: 0.8; } -.funds-card__bar-threshold { +.flows-card__bar--outcome { opacity: 0.8; } +.flows-card__bar-threshold { position: absolute; top: -3px; width: 2px; height: 12px; background: #fbbf24; border-radius: 1px; } -.funds-card__thresholds { +.flows-card__thresholds { display: flex; gap: 12px; font-size: 11px; color: #64748b; margin-bottom: 8px; } /* Allocation lists */ -.funds-card__allocs { margin-top: 8px; padding-top: 8px; border-top: 1px solid #334155; } -.funds-card__alloc-title { font-size: 11px; color: #64748b; font-weight: 600; margin-bottom: 4px; text-transform: uppercase; } -.funds-card__alloc { font-size: 12px; color: #94a3b8; display: flex; align-items: center; gap: 6px; margin: 2px 0; } -.funds-card__alloc-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; } +.flows-card__allocs { margin-top: 8px; padding-top: 8px; border-top: 1px solid #334155; } +.flows-card__alloc-title { font-size: 11px; color: #64748b; font-weight: 600; margin-bottom: 4px; text-transform: uppercase; } +.flows-card__alloc { font-size: 12px; color: #94a3b8; display: flex; align-items: center; gap: 6px; margin: 2px 0; } +.flows-card__alloc-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; } /* Status colors */ -.funds-status--abundant { color: #fbbf24; } -.funds-status--sufficient { color: #10b981; } -.funds-status--seeking { color: #0ea5e9; } -.funds-status--critical { color: #ef4444; } +.flows-status--abundant { color: #fbbf24; } +.flows-status--sufficient { color: #10b981; } +.flows-status--seeking { color: #0ea5e9; } +.flows-status--critical { color: #ef4444; } /* ── Interactive canvas (Diagram tab) ───────────────── */ -.funds-canvas-container { +.flows-canvas-container { position: relative; height: 70vh; min-height: 400px; background: #0f172a; border-radius: 12px; border: 1px solid #334155; - overflow: hidden; user-select: none; + overflow: hidden; user-select: none; touch-action: none; } -.funds-canvas-svg { +.flows-canvas-svg { width: 100%; height: 100%; display: block; cursor: grab; } -.funds-canvas-svg.panning { cursor: grabbing; } -.funds-canvas-svg.dragging { cursor: move; } +.flows-canvas-svg.panning { cursor: grabbing; } +.flows-canvas-svg.dragging { cursor: move; } /* Toolbar — top-right overlay */ -.funds-canvas-toolbar { +.flows-canvas-toolbar { position: absolute; top: 10px; right: 10px; z-index: 10; display: flex; gap: 4px; flex-wrap: wrap; align-items: center; } -.funds-canvas-btn { +.flows-canvas-btn { padding: 5px 10px; border: 1px solid #475569; border-radius: 6px; background: #1e293b; color: #e2e8f0; font-size: 11px; font-weight: 500; cursor: pointer; white-space: nowrap; transition: background 0.15s, border-color 0.15s; } -.funds-canvas-btn:hover { background: #334155; border-color: #64748b; } -.funds-canvas-btn--source { border-color: #10b981; color: #6ee7b7; } -.funds-canvas-btn--source:hover { background: #064e3b; } -.funds-canvas-btn--funnel { border-color: #3b82f6; color: #93c5fd; } -.funds-canvas-btn--funnel:hover { background: #1e3a5f; } -.funds-canvas-btn--outcome { border-color: #ec4899; color: #f9a8d4; } -.funds-canvas-btn--outcome:hover { background: #4a1942; } -.funds-canvas-btn--active { background: #4f46e5; border-color: #6366f1; color: #fff; } -.funds-canvas-sep { +.flows-canvas-btn:hover { background: #334155; border-color: #64748b; } +.flows-canvas-btn--source { border-color: #10b981; color: #6ee7b7; } +.flows-canvas-btn--source:hover { background: #064e3b; } +.flows-canvas-btn--funnel { border-color: #3b82f6; color: #93c5fd; } +.flows-canvas-btn--funnel:hover { background: #1e3a5f; } +.flows-canvas-btn--outcome { border-color: #ec4899; color: #f9a8d4; } +.flows-canvas-btn--outcome:hover { background: #4a1942; } +.flows-canvas-btn--active { background: #4f46e5; border-color: #6366f1; color: #fff; } +.flows-canvas-sep { width: 1px; height: 20px; background: #334155; margin: 0 4px; } @@ -183,14 +196,14 @@ .node-glow { filter: drop-shadow(0 0 6px rgba(251,191,36,0.5)); } /* Editor panel — right side slide-in */ -.funds-editor-panel { +.flows-editor-panel { position: absolute; top: 0; right: 0; bottom: 0; width: 320px; z-index: 20; background: #1e293b; border-left: 1px solid #334155; transform: translateX(100%); transition: transform 0.25s ease; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; } -.funds-editor-panel.open { transform: translateX(0); } +.flows-editor-panel.open { transform: translateX(0); } .editor-header { display: flex; align-items: center; justify-content: space-between; @@ -243,59 +256,59 @@ .edge-pct { color: #e2e8f0; font-weight: 600; min-width: 30px; text-align: center; } /* Legend — bottom-left */ -.funds-canvas-legend { +.flows-canvas-legend { position: absolute; bottom: 10px; left: 10px; z-index: 10; display: flex; flex-wrap: wrap; gap: 12px; font-size: 11px; color: #94a3b8; background: rgba(15,23,42,0.85); padding: 6px 10px; border-radius: 8px; } -.funds-canvas-legend-item { display: flex; align-items: center; gap: 4px; } -.funds-canvas-legend-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; } +.flows-canvas-legend-item { display: flex; align-items: center; gap: 4px; } +.flows-canvas-legend-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; } /* Zoom controls — bottom-right */ -.funds-canvas-zoom { +.flows-canvas-zoom { position: absolute; bottom: 10px; right: 10px; z-index: 10; display: flex; gap: 4px; } /* Sufficiency badge — top-left */ -.funds-canvas-badge { +.flows-canvas-badge { position: absolute; top: 10px; left: 10px; z-index: 10; - background: rgba(15,23,42,0.85); border-radius: 10px; padding: 8px 14px; + background: rgba(15,23,42,0.85); border-radius: 8px; padding: 8px 14px; display: flex; align-items: center; gap: 8px; } -.funds-canvas-badge__score { font-size: 20px; font-weight: 700; } -.funds-canvas-badge__label { font-size: 10px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; } +.flows-canvas-badge__score { font-size: 20px; font-weight: 700; } +.flows-canvas-badge__label { font-size: 10px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; } /* Legacy diagram (kept for compat) */ -.funds-diagram { overflow-x: auto; } -.funds-diagram svg { display: block; margin: 0 auto; } -.funds-diagram__legend { +.flows-diagram { overflow-x: auto; } +.flows-diagram svg { display: block; margin: 0 auto; } +.flows-diagram__legend { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; margin-top: 12px; font-size: 12px; color: #94a3b8; } -.funds-diagram__legend-item { display: flex; align-items: center; gap: 5px; } -.funds-diagram__dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; } +.flows-diagram__legend-item { display: flex; align-items: center; gap: 5px; } +.flows-diagram__dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; } /* ── River tab ───────────────────────────────────────── */ -.funds-river-container { min-height: 500px; } +.flows-river-container { min-height: 500px; } /* ── Transactions tab ────────────────────────────────── */ -.funds-tx-list { display: flex; flex-direction: column; gap: 4px; } -.funds-tx-empty { text-align: center; color: #64748b; padding: 48px 16px; font-size: 14px; } +.flows-tx-list { display: flex; flex-direction: column; gap: 4px; } +.flows-tx-empty { text-align: center; color: #64748b; padding: 48px 16px; font-size: 14px; } -.funds-tx { +.flows-tx { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: #1e293b; border: 1px solid #334155; border-radius: 8px; } -.funds-tx__icon { font-size: 16px; flex-shrink: 0; } -.funds-tx__body { flex: 1; min-width: 0; } -.funds-tx__desc { font-size: 13px; color: #e2e8f0; font-weight: 500; } -.funds-tx__meta { font-size: 11px; color: #64748b; margin-top: 2px; } -.funds-tx__amount { font-size: 14px; font-weight: 600; white-space: nowrap; } -.funds-tx__amount--positive { color: #10b981; } -.funds-tx__amount--negative { color: #ef4444; } -.funds-tx__time { font-size: 11px; color: #64748b; white-space: nowrap; } +.flows-tx__icon { font-size: 16px; flex-shrink: 0; } +.flows-tx__body { flex: 1; min-width: 0; } +.flows-tx__desc { font-size: 13px; color: #e2e8f0; font-weight: 500; } +.flows-tx__meta { font-size: 11px; color: #64748b; margin-top: 2px; } +.flows-tx__amount { font-size: 14px; font-weight: 600; white-space: nowrap; } +.flows-tx__amount--positive { color: #10b981; } +.flows-tx__amount--negative { color: #ef4444; } +.flows-tx__time { font-size: 11px; color: #64748b; white-space: nowrap; } /* ── Port & wiring ──────────────────────────────────── */ .port-group { pointer-events: all; } @@ -312,7 +325,7 @@ stroke-linecap: round; animation: wire-dash 0.6s linear infinite; } -.funds-canvas-svg.wiring { cursor: crosshair; } +.flows-canvas-svg.wiring { cursor: crosshair; } @keyframes port-glow { 0%, 100% { filter: drop-shadow(0 0 4px currentColor); } @@ -342,40 +355,40 @@ .satisfaction-bar-fill { transition: width 0.3s ease; } /* ── Node detail modals ──────────────────────────────── */ -.funds-modal-backdrop { +.flows-modal-backdrop { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; animation: modalFadeIn 0.15s ease-out; } @keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } } -.funds-modal { +.flows-modal { background: #1e293b; border-radius: 16px; padding: 24px; width: 440px; max-height: 85vh; overflow-y: auto; border: 1px solid #334155; box-shadow: 0 20px 60px rgba(0,0,0,0.5); animation: modalSlideIn 0.2s ease-out; } @keyframes modalSlideIn { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } } -.funds-modal::-webkit-scrollbar { width: 6px; } -.funds-modal::-webkit-scrollbar-track { background: transparent; } -.funds-modal::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; } -.funds-modal__header { +.flows-modal::-webkit-scrollbar { width: 6px; } +.flows-modal::-webkit-scrollbar-track { background: transparent; } +.flows-modal::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; } +.flows-modal__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } -.funds-modal__close { +.flows-modal__close { background: none; border: none; color: #94a3b8; font-size: 24px; cursor: pointer; padding: 2px 8px; border-radius: 4px; transition: color 0.15s; } -.funds-modal__close:hover { color: #e2e8f0; } -.funds-modal__progress-bar { +.flows-modal__close:hover { color: #e2e8f0; } +.flows-modal__progress-bar { height: 8px; background: #334155; border-radius: 4px; overflow: hidden; margin-top: 8px; } -.funds-modal__progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; } +.flows-modal__progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; } /* Phase accordion */ .phase-tier-bar { display: flex; gap: 1px; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 16px; } .phase-tier-segment { flex: 1; transition: background 0.3s; } -.phase-card { border: 1px solid #334155; border-radius: 10px; overflow: hidden; margin-bottom: 8px; } +.phase-card { border: 1px solid #334155; border-radius: 8px; overflow: hidden; margin-bottom: 8px; } .phase-card--locked { opacity: 0.5; } .phase-header { padding: 10px 14px; cursor: pointer; display: flex; align-items: center; gap: 8px; @@ -399,7 +412,7 @@ .source-type-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; } .source-type-btn { display: flex; flex-direction: column; align-items: center; gap: 6px; - padding: 14px 8px; border-radius: 10px; border: 2px solid #334155; + padding: 14px 8px; border-radius: 8px; border: 2px solid #334155; background: #0f172a; color: #94a3b8; cursor: pointer; transition: all 0.15s; font-size: 12px; font-weight: 500; } @@ -407,14 +420,14 @@ .source-type-btn--active { border-color: #10b981; background: #064e3b; color: #6ee7b7; } /* Node hover tooltip */ -.funds-node-tooltip { +.flows-node-tooltip { position: absolute; z-index: 30; pointer-events: none; background: rgba(15,23,42,0.95); border: 1px solid #475569; border-radius: 8px; padding: 8px 12px; font-size: 12px; color: #e2e8f0; box-shadow: 0 4px 12px rgba(0,0,0,0.4); white-space: nowrap; } -.funds-node-tooltip__label { font-weight: 600; margin-bottom: 2px; } -.funds-node-tooltip__stat { color: #94a3b8; font-size: 11px; } +.flows-node-tooltip__label { font-weight: 600; margin-bottom: 2px; } +.flows-node-tooltip__stat { color: #94a3b8; font-size: 11px; } /* Sufficiency glow on funnel status text */ @keyframes sufficiencyPulse { @@ -425,14 +438,17 @@ /* ── Mobile responsive ──────────────────────────────── */ @media (max-width: 768px) { - .funds-diagram { overflow-x: auto; -webkit-overflow-scrolling: touch; } - .funds-flows__grid { grid-template-columns: 1fr; } - .funds-features__grid { grid-template-columns: 1fr; } - .funds-cards { grid-template-columns: 1fr; } - .funds-tabs { flex-wrap: wrap; } - .funds-canvas-container { height: 50vh; min-height: 300px; } - .funds-canvas-toolbar { flex-wrap: wrap; gap: 3px; top: 6px; right: 6px; } - .funds-canvas-btn { padding: 4px 7px; font-size: 10px; } - .funds-editor-panel { width: 100%; } - .funds-canvas-legend { font-size: 10px; gap: 8px; } + .flows-diagram { overflow-x: auto; -webkit-overflow-scrolling: touch; } + .flows-flows__grid { grid-template-columns: 1fr; } + .flows-features__grid { grid-template-columns: 1fr; } + .flows-cards { grid-template-columns: 1fr; } + .flows-tabs { flex-wrap: wrap; } + .flows-tab { min-height: 44px; min-width: 44px; display: flex; align-items: center; justify-content: center; } + .flows-canvas-container { height: 50vh; min-height: 300px; } + .flows-canvas-toolbar { flex-wrap: wrap; gap: 3px; top: 6px; right: 6px; } + .flows-canvas-btn { padding: 6px 10px; font-size: 11px; min-height: 44px; min-width: 44px; } + .flows-editor-panel { width: 100%; } + .flows-canvas-legend { font-size: 10px; gap: 8px; } + .flows-landing { padding: 16px 12px 48px; } + .flows-detail { padding: 12px 12px 48px; } } diff --git a/modules/rflows/components/folk-flow-river.ts b/modules/rflows/components/folk-flow-river.ts index 0f0fc60..dd086c7 100644 --- a/modules/rflows/components/folk-flow-river.ts +++ b/modules/rflows/components/folk-flow-river.ts @@ -1,7 +1,7 @@ /** - * — animated SVG sankey river visualization. + * — animated SVG sankey river visualization. * Pure renderer: receives nodes via setNodes() or falls back to demo data. - * Parent component (folk-funds-app) handles data fetching and mapping. + * Parent component (folk-flows-app) handles data fetching and mapping. */ import type { FlowNode, FunnelNodeData, OutcomeNodeData, SourceNodeData, SufficiencyState } from "../lib/types"; @@ -382,7 +382,7 @@ function esc(s: string): string { // ─── Web Component ────────────────────────────────────── -class FolkBudgetRiver extends HTMLElement { +class FolkFlowRiver extends HTMLElement { private shadow: ShadowRoot; private nodes: FlowNode[] = []; private simulating = false; @@ -521,4 +521,4 @@ class FolkBudgetRiver extends HTMLElement { } } -customElements.define("folk-budget-river", FolkBudgetRiver); +customElements.define("folk-flow-river", FolkFlowRiver); diff --git a/modules/rflows/components/folk-flows-app.ts b/modules/rflows/components/folk-flows-app.ts index 2584f7d..0be2cc3 100644 --- a/modules/rflows/components/folk-flows-app.ts +++ b/modules/rflows/components/folk-flows-app.ts @@ -1,5 +1,5 @@ /** - * — main rFunds application component. + * — main rFlows application component. * * Views: * "landing" — TBFF info hero + flow list cards @@ -56,7 +56,7 @@ function isAuthenticated(): boolean { return getSession() !== null; } function getAccessToken(): string | null { return getSession()?.accessToken ?? null; } function getUsername(): string | null { return getSession()?.claims?.username ?? null; } -class FolkFundsApp extends HTMLElement { +class FolkFlowsApp extends HTMLElement { private shadow: ShadowRoot; private space = ""; private view: View = "landing"; @@ -101,6 +101,11 @@ class FolkFundsApp extends HTMLElement { private wiringPointerX = 0; private wiringPointerY = 0; + // Touch gesture state (two-finger pinch-to-zoom & pan) + private isTouchPanning = false; + private lastTouchCenter: { x: number; y: number } | null = null; + private lastTouchDist: number | null = null; + // Bound handlers for cleanup private _boundKeyDown: ((e: KeyboardEvent) => void) | null = null; private _boundPointerMove: ((e: PointerEvent) => void) | null = null; @@ -132,8 +137,8 @@ class FolkFundsApp extends HTMLElement { private getApiBase(): string { const path = window.location.pathname; - // Subdomain: /rfunds/... or Direct: /{space}/rfunds/... - const match = path.match(/^(\/[^/]+)?\/rfunds/); + // Subdomain: /rflows/... or Direct: /{space}/rflows/... + const match = path.match(/^(\/[^/]+)?\/rflows/); return match ? `${match[0]}` : ""; } @@ -195,9 +200,9 @@ class FolkFundsApp extends HTMLElement { } private getCssPath(): string { - // In rSpace: /modules/rfunds/funds.css | Standalone: /modules/rfunds/funds.css - // The shell always serves from /modules/rfunds/ in both modes - return "/modules/rfunds/funds.css"; + // In rSpace: /modules/rflows/flows.css | Standalone: /modules/rflows/flows.css + // The shell always serves from /modules/rflows/ in both modes + return "/modules/rflows/flows.css"; } private render() { @@ -207,8 +212,8 @@ class FolkFundsApp extends HTMLElement { *, *::before, *::after { box-sizing: border-box; } - ${this.error ? `
${this.esc(this.error)}
` : ""} - ${this.loading && this.view === "landing" ? '
Loading...
' : ""} + ${this.error ? `
${this.esc(this.error)}
` : ""} + ${this.loading && this.view === "landing" ? '
Loading...
' : ""} ${this.renderView()} `; this.attachListeners(); @@ -222,12 +227,12 @@ class FolkFundsApp extends HTMLElement { // ─── Landing page ────────────────────────────────────── private renderLanding(): string { - const demoUrl = this.getApiBase() ? `${this.getApiBase()}/demo` : "/rfunds/demo"; + const demoUrl = this.getApiBase() ? `${this.getApiBase()}/demo` : "/rflows/demo"; const authed = isAuthenticated(); const username = getUsername(); return ` -
+
Flows
@@ -239,53 +244,53 @@ class FolkFundsApp extends HTMLElement {
-
+
Design transparent resource flows with sufficiency-based cascading. Funnels fill to their threshold, then overflow routes surplus to the next layer — ensuring every level has enough before abundance cascades forward.
-
-
-
-
💰
+
+
+
+
💰

Sources

Revenue streams split across funnels by configurable allocation percentages.

-
-
🏛
+
+
🏛

Funnels

Budget buckets with min/max thresholds and sufficiency-based overflow cascading.

-
-
🎯
+
+
🎯

Outcomes

Funding targets that receive spending allocations. Track progress toward each goal.

-
-
🌊
+
+
🌊

River View

Animated sankey diagram showing live fund flows through your entire system.

-
-
+
+

Enoughness

System-wide sufficiency scoring. Golden glow when funnels reach their threshold.

-
-
-

${authed ? `Flows in ${this.esc(this.space)}` : "Your Flows"}

- ${authed ? `Signed in as ${this.esc(username || "")}` : ""} +
+
+

${authed ? `Flows in ${this.esc(this.space)}` : "Your Flows"}

+ ${authed ? `Signed in as ${this.esc(username || "")}` : ""}
${this.flows.length > 0 ? ` -
+
${this.flows.map((f) => this.renderFlowCard(f)).join("")}
` : ` -
+
${authed ? `

No flows in this space yet.

Explore the demo or create your first flow.

` @@ -295,25 +300,25 @@ class FolkFundsApp extends HTMLElement { `}
-
-

How TBFF Works

-
-
-
1
+
+

How TBFF Works

+
+
+
1

Define Sources

Add revenue streams — grants, donations, sales, or any recurring income — with allocation splits.

-
-
2
+
+
2

Configure Funnels

Set minimum, sufficient, and maximum thresholds. Overflow rules determine where surplus flows.

-
-
3
+
+
3

Track Outcomes

Funding targets receive allocations as funnels reach sufficiency. Watch the river flow in real time.

@@ -327,14 +332,14 @@ class FolkFundsApp extends HTMLElement { private renderFlowCard(f: FlowSummary): string { const detailUrl = this.getApiBase() ? `${this.getApiBase()}/flow/${encodeURIComponent(f.id)}` - : `/rfunds/flow/${encodeURIComponent(f.id)}`; + : `/rflows/flow/${encodeURIComponent(f.id)}`; const value = f.totalValue != null ? `$${Math.floor(f.totalValue).toLocaleString()}` : ""; return ` - -
${this.esc(f.name || f.label || f.id)}
- ${value ? `
${value}
` : ""} -
+ +
${this.esc(f.name || f.label || f.id)}
+ ${value ? `
${value}
` : ""} +
${f.funnelCount != null ? `${f.funnelCount} funnels` : ""} ${f.outcomeCount != null ? ` · ${f.outcomeCount} outcomes` : ""} ${f.status ? ` · ${f.status}` : ""} @@ -347,25 +352,25 @@ class FolkFundsApp extends HTMLElement { private renderDetail(): string { const backUrl = this.getApiBase() ? `${this.getApiBase()}/` - : "/rfunds/"; + : "/rflows/"; return ` -
+
← Flows ${this.esc(this.flowName || "Flow Detail")} ${this.isDemo ? 'Demo' : ""}
-
- - - - +
+ + + +
-
- ${this.loading ? '
Loading...
' : this.renderTab()} +
+ ${this.loading ? '
Loading...
' : this.renderTab()}
`; } @@ -385,26 +390,26 @@ class FolkFundsApp extends HTMLElement { const sources = this.nodes.filter((n) => n.type === "source"); return ` -
+
${sources.length > 0 ? ` -
-

Sources

-
+
+

Sources

+
${sources.map((n) => this.renderSourceCard(n.data as SourceNodeData, n.id)).join("")}
` : ""} -
-

Funnels

-
+
+

Funnels

+
${funnels.map((n) => this.renderFunnelCard(n.data as FunnelNodeData, n.id)).join("")}
-
-

Outcomes

-
+
+

Outcomes

+
${outcomes.map((n) => this.renderOutcomeCard(n.data as OutcomeNodeData, n.id)).join("")}
@@ -414,21 +419,21 @@ class FolkFundsApp extends HTMLElement { private renderSourceCard(data: SourceNodeData, id: string): string { const allocations = data.targetAllocations || []; return ` -
-
- 💰 - ${this.esc(data.label)} - ${data.sourceType} +
+
+ 💰 + ${this.esc(data.label)} + ${data.sourceType}
-
- $${data.flowRate.toLocaleString()} - /month +
+ $${data.flowRate.toLocaleString()} + /month
${allocations.length > 0 ? ` -
+
${allocations.map((a) => ` -
- +
+ ${a.percentage}% → ${this.esc(this.getNodeLabel(a.targetId))}
`).join("")} @@ -443,10 +448,10 @@ class FolkFundsApp extends HTMLElement { const fillPct = Math.min(100, (data.currentValue / (data.maxCapacity || 1)) * 100); const suffPct = Math.min(100, (data.currentValue / (threshold || 1)) * 100); - const statusClass = sufficiency === "abundant" ? "funds-status--abundant" - : sufficiency === "sufficient" ? "funds-status--sufficient" - : data.currentValue < data.minThreshold ? "funds-status--critical" - : "funds-status--seeking"; + const statusClass = sufficiency === "abundant" ? "flows-status--abundant" + : sufficiency === "sufficient" ? "flows-status--sufficient" + : data.currentValue < data.minThreshold ? "flows-status--critical" + : "flows-status--seeking"; const statusLabel = sufficiency === "abundant" ? "Abundant" : sufficiency === "sufficient" ? "Sufficient" @@ -454,48 +459,48 @@ class FolkFundsApp extends HTMLElement { : "Seeking"; return ` -
-
- 🏛 - ${this.esc(data.label)} - ${statusLabel} +
+
+ 🏛 + ${this.esc(data.label)} + ${statusLabel}
-
-
-
+
+
+
-
+
- $${Math.floor(data.currentValue).toLocaleString()} - / $${Math.floor(threshold).toLocaleString()} + $${Math.floor(data.currentValue).toLocaleString()} + / $${Math.floor(threshold).toLocaleString()}
- ${Math.round(suffPct)}% - sufficiency + ${Math.round(suffPct)}% + sufficiency
-
+
Min: $${Math.floor(data.minThreshold).toLocaleString()} Max: $${Math.floor(data.maxThreshold).toLocaleString()} Cap: $${Math.floor(data.maxCapacity).toLocaleString()}
${data.overflowAllocations.length > 0 ? ` -
-
Overflow
+
+
Overflow
${data.overflowAllocations.map((a) => ` -
- +
+ ${a.percentage}% → ${this.esc(this.getNodeLabel(a.targetId))}
`).join("")}
` : ""} ${data.spendingAllocations.length > 0 ? ` -
-
Spending
+
+
Spending
${data.spendingAllocations.map((a) => ` -
- +
+ ${a.percentage}% → ${this.esc(this.getNodeLabel(a.targetId))}
`).join("")} @@ -514,24 +519,24 @@ class FolkFundsApp extends HTMLElement { : "#64748b"; return ` -
-
- 🎯 - ${this.esc(data.label)} - ${data.status} +
+
+ 🎯 + ${this.esc(data.label)} + ${data.status}
- ${data.description ? `
${this.esc(data.description)}
` : ""} -
-
+ ${data.description ? `
${this.esc(data.description)}
` : ""} +
+
-
+
- $${Math.floor(data.fundingReceived).toLocaleString()} - / $${Math.floor(data.fundingTarget).toLocaleString()} + $${Math.floor(data.fundingReceived).toLocaleString()} + / $${Math.floor(data.fundingTarget).toLocaleString()}
- ${Math.round(fillPct)}% - funded + ${Math.round(fillPct)}% + funded
`; @@ -547,7 +552,7 @@ class FolkFundsApp extends HTMLElement { private renderDiagramTab(): string { if (this.nodes.length === 0) { - return '
No nodes to display.
'; + return '
No nodes to display.
'; } const score = computeSystemSufficiency(this.nodes); @@ -555,43 +560,43 @@ class FolkFundsApp extends HTMLElement { const scoreColor = scorePct >= 90 ? "#fbbf24" : scorePct >= 60 ? "#10b981" : scorePct >= 30 ? "#f59e0b" : "#ef4444"; return ` -
-
+
+
-
${scorePct}%
-
ENOUGH
+
${scorePct}%
+
ENOUGH
-
- - - -
- - - +
+ + + +
+ + +
- + -
-
- Source - Funnel - Overflow - Spending - Outcome - Sufficient +
+
+ Source + Funnel + Overflow + Spending + Outcome + Sufficient
-
- - +
+ +
- +
`; } @@ -896,6 +901,72 @@ class FolkFundsApp extends HTMLElement { }); } + // Touch gesture handling for two-finger pan + pinch-to-zoom + const getTouchCenter = (touches: TouchList) => ({ + x: (touches[0].clientX + touches[1].clientX) / 2, + y: (touches[0].clientY + touches[1].clientY) / 2, + }); + const getTouchDist = (touches: TouchList) => { + const dx = touches[0].clientX - touches[1].clientX; + const dy = touches[0].clientY - touches[1].clientY; + return Math.hypot(dx, dy); + }; + + svg.addEventListener("touchstart", (e: TouchEvent) => { + if (e.touches.length === 2) { + e.preventDefault(); + this.isTouchPanning = true; + // Cancel any pointer-based pan or node drag + this.isPanning = false; + if (this.draggingNodeId) { + this.draggingNodeId = null; + nodeDragStarted = false; + svg.classList.remove("dragging"); + } + if (this.wiringActive) this.cancelWiring(); + this.lastTouchCenter = getTouchCenter(e.touches); + this.lastTouchDist = getTouchDist(e.touches); + } + }, { passive: false }); + + svg.addEventListener("touchmove", (e: TouchEvent) => { + if (e.touches.length === 2 && this.isTouchPanning) { + e.preventDefault(); + const currentCenter = getTouchCenter(e.touches); + const currentDist = getTouchDist(e.touches); + + if (this.lastTouchCenter) { + // Two-finger pan + this.canvasPanX += currentCenter.x - this.lastTouchCenter.x; + this.canvasPanY += currentCenter.y - this.lastTouchCenter.y; + } + + if (this.lastTouchDist && this.lastTouchDist > 0) { + // Pinch-to-zoom around gesture center + const zoomDelta = currentDist / this.lastTouchDist; + const newZoom = Math.max(0.2, Math.min(5, this.canvasZoom * zoomDelta)); + const rect = svg.getBoundingClientRect(); + const cx = currentCenter.x - rect.left; + const cy = currentCenter.y - rect.top; + this.canvasPanX = cx - (cx - this.canvasPanX) * (newZoom / this.canvasZoom); + this.canvasPanY = cy - (cy - this.canvasPanY) * (newZoom / this.canvasZoom); + this.canvasZoom = newZoom; + } + + this.lastTouchCenter = currentCenter; + this.lastTouchDist = currentDist; + this.updateCanvasTransform(); + } + }, { passive: false }); + + svg.addEventListener("touchend", (e: TouchEvent) => { + if (e.touches.length < 2) { + this.lastTouchCenter = null; + this.lastTouchDist = null; + this.isTouchPanning = false; + } + }); + // Keyboard this._boundKeyDown = (e: KeyboardEvent) => { // Skip if typing in editor input @@ -1665,7 +1736,7 @@ class FolkFundsApp extends HTMLElement { let apiKey = "STAGING_KEY"; let env = "STAGING"; try { - const base = this.space ? `/s/${this.space}/rfunds` : "/rfunds"; + const base = this.space ? `/s/${this.space}/rflows` : "/rflows"; const res = await fetch(`${base}/api/transak/config`); if (res.ok) { const cfg = await res.json(); @@ -1762,20 +1833,20 @@ class FolkFundsApp extends HTMLElement { const node = this.nodes.find((n) => n.id === nodeId); if (!node) return; - let html = `
${this.esc((node.data as any).label)}
`; + let html = `
${this.esc((node.data as any).label)}
`; if (node.type === "source") { const d = node.data as SourceNodeData; - html += `
$${d.flowRate.toLocaleString()}/mo · ${d.sourceType}
`; + html += `
$${d.flowRate.toLocaleString()}/mo · ${d.sourceType}
`; } else if (node.type === "funnel") { const d = node.data as FunnelNodeData; const suf = computeSufficiencyState(d); - html += `
$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(d.sufficientThreshold ?? d.maxThreshold).toLocaleString()}
`; - html += `
${suf}
`; + html += `
$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(d.sufficientThreshold ?? d.maxThreshold).toLocaleString()}
`; + html += `
${suf}
`; } else { const d = node.data as OutcomeNodeData; const pct = d.fundingTarget > 0 ? Math.round((d.fundingReceived / d.fundingTarget) * 100) : 0; - html += `
$${Math.floor(d.fundingReceived).toLocaleString()} / $${Math.floor(d.fundingTarget).toLocaleString()} (${pct}%)
`; - html += `
${d.status}
`; + html += `
$${Math.floor(d.fundingReceived).toLocaleString()} / $${Math.floor(d.fundingTarget).toLocaleString()} (${pct}%)
`; + html += `
${d.status}
`; } tooltip.innerHTML = html; @@ -1811,7 +1882,7 @@ class FolkFundsApp extends HTMLElement { // ─── Node detail modals ────────────────────────────── private closeModal() { - const m = this.shadow.getElementById("funds-modal"); + const m = this.shadow.getElementById("flows-modal"); if (m) m.remove(); } @@ -1858,8 +1929,8 @@ class FolkFundsApp extends HTMLElement { ${Math.min(phasePct, 100)}% funded $${Math.floor(Math.min(d.fundingReceived, p.fundingThreshold)).toLocaleString()} / $${p.fundingThreshold.toLocaleString()}
-
-
+
+
${p.tasks.map((t, ti) => ` @@ -1877,22 +1948,22 @@ class FolkFundsApp extends HTMLElement { } const backdrop = document.createElement("div"); - backdrop.className = "funds-modal-backdrop"; - backdrop.id = "funds-modal"; - backdrop.innerHTML = `
-
+ backdrop.className = "flows-modal-backdrop"; + backdrop.id = "flows-modal"; + backdrop.innerHTML = `
+
${statusLabel} ${this.esc(d.label)}
- +
${d.description ? `
${this.esc(d.description)}
` : ""}
$${Math.floor(d.fundingReceived).toLocaleString()}
of $${Math.floor(d.fundingTarget).toLocaleString()} (${Math.round(fillPct)}%)
-
-
+
+
${d.phases && d.phases.length > 0 ? `
@@ -2018,15 +2089,15 @@ class FolkFundsApp extends HTMLElement { } const backdrop = document.createElement("div"); - backdrop.className = "funds-modal-backdrop"; - backdrop.id = "funds-modal"; - backdrop.innerHTML = `
-
+ backdrop.className = "flows-modal-backdrop"; + backdrop.id = "flows-modal"; + backdrop.innerHTML = `
+
${icons[d.sourceType] || "💰"} ${this.esc(d.label)}
- +
Source Type
@@ -2258,7 +2329,7 @@ class FolkFundsApp extends HTMLElement { // ─── River tab ──────────────────────────────────────── private renderRiverTab(): string { - return `
`; + return `
`; } private mountRiver() { @@ -2266,9 +2337,9 @@ class FolkFundsApp extends HTMLElement { if (!mount) return; // Check if already mounted - if (mount.querySelector("folk-budget-river")) return; + if (mount.querySelector("folk-flow-river")) return; - const river = document.createElement("folk-budget-river") as any; + const river = document.createElement("folk-flow-river") as any; river.setAttribute("simulate", "true"); mount.appendChild(river); @@ -2285,38 +2356,38 @@ class FolkFundsApp extends HTMLElement { private renderTransactionsTab(): string { if (this.isDemo) { return ` -
+

Transaction history is not available in demo mode.

`; } if (!this.txLoaded) { - return '
Loading transactions...
'; + return '
Loading transactions...
'; } if (this.transactions.length === 0) { return ` -
+

No transactions yet for this flow.

`; } return ` -
+
${this.transactions.map((tx) => ` -
-
${tx.type === "deposit" ? "⬆" : tx.type === "withdraw" ? "⬇" : "🔄"}
-
-
${this.esc(tx.description || tx.type)}
-
+
+
${tx.type === "deposit" ? "⬆" : tx.type === "withdraw" ? "⬇" : "🔄"}
+
+
${this.esc(tx.description || tx.type)}
+
${tx.from ? `From: ${this.esc(tx.from)}` : ""} ${tx.to ? ` → ${this.esc(tx.to)}` : ""}
-
+
${tx.type === "deposit" ? "+" : "-"}$${Math.abs(tx.amount).toLocaleString()}
-
${this.formatTime(tx.timestamp)}
+
${this.formatTime(tx.timestamp)}
`).join("")}
`; @@ -2415,7 +2486,7 @@ class FolkFundsApp extends HTMLElement { if (flowId) { const detailUrl = this.getApiBase() ? `${this.getApiBase()}/flow/${encodeURIComponent(flowId)}` - : `/rfunds/flow/${encodeURIComponent(flowId)}`; + : `/rflows/flow/${encodeURIComponent(flowId)}`; window.location.href = detailUrl; return; } @@ -2439,4 +2510,4 @@ class FolkFundsApp extends HTMLElement { } } -customElements.define("folk-funds-app", FolkFundsApp); +customElements.define("folk-flows-app", FolkFlowsApp); diff --git a/modules/rflows/landing.ts b/modules/rflows/landing.ts index 1894451..598d0a3 100644 --- a/modules/rflows/landing.ts +++ b/modules/rflows/landing.ts @@ -1,6 +1,6 @@ /** - * rFunds — rich landing page body. - * Ported from rfunds-online/app/page.tsx (Next.js/Tailwind). + * rFlows — rich landing page body. + * Ported from rflows-online/app/page.tsx (Next.js/Tailwind). * Returned by landingPage() in the module export; * the shell wraps it with header, CSS, and analytics. */ @@ -8,7 +8,7 @@ export function renderLanding(): string { return `
- rFunds + rFlows

Threshold-Based Flow Funding

@@ -17,7 +17,7 @@ export function renderLanding(): string { Connect funnels, set overflow rules, and track outcomes in real-time.

@@ -120,7 +120,7 @@ export function renderLanding(): string {

Ecosystem Integration

- rFunds connects to other rSpace modules for end-to-end treasury governance. + rFlows connects to other rSpace modules for end-to-end treasury governance.

@@ -145,7 +145,7 @@ export function renderLanding(): string {

Built on Open Source

-

The libraries and tools that power rFunds.

+

The libraries and tools that power rFlows.

🌊
@@ -170,7 +170,7 @@ export function renderLanding(): string {

Your Data, Protected

-

How rFunds keeps your information safe.

+

How rFlows keeps your information safe.

🔒
@@ -199,7 +199,7 @@ export function renderLanding(): string {

Ready to design your funding flows?

Start with the interactive demo or create your own space with custom funnels and connections.

diff --git a/modules/rflows/lib/map-flow.ts b/modules/rflows/lib/map-flow.ts index 2306625..4d8579b 100644 --- a/modules/rflows/lib/map-flow.ts +++ b/modules/rflows/lib/map-flow.ts @@ -1,6 +1,6 @@ /** * Maps TBFF API response data to FlowNode[] for visualization. - * Shared between folk-funds-app (data loading) and folk-budget-river (rendering). + * Shared between folk-flows-app (data loading) and folk-flow-river (rendering). */ import type { FlowNode, FunnelNodeData, OutcomeNodeData, SourceNodeData } from "./types"; diff --git a/modules/rflows/lib/presets.ts b/modules/rflows/lib/presets.ts index ea8ca07..b47cef2 100644 --- a/modules/rflows/lib/presets.ts +++ b/modules/rflows/lib/presets.ts @@ -1,5 +1,5 @@ /** - * Demo presets — ported from rfunds-online/lib/presets.ts. + * Demo presets — ported from rflows-online/lib/presets.ts. */ import type { FlowNode, FunnelNodeData, OutcomeNodeData, OutcomePhase, SourceNodeData } from "./types"; diff --git a/modules/rflows/lib/simulation.ts b/modules/rflows/lib/simulation.ts index 4298a54..66d9f6f 100644 --- a/modules/rflows/lib/simulation.ts +++ b/modules/rflows/lib/simulation.ts @@ -1,6 +1,6 @@ /** * Flow simulation engine — pure function, no framework dependencies. - * Ported from rfunds-online/lib/simulation.ts. + * Ported from rflows-online/lib/simulation.ts. */ import type { FlowNode, FunnelNodeData, OutcomeNodeData, SufficiencyState } from "./types"; diff --git a/modules/rflows/lib/types.ts b/modules/rflows/lib/types.ts index f5875ac..b0f75f3 100644 --- a/modules/rflows/lib/types.ts +++ b/modules/rflows/lib/types.ts @@ -1,5 +1,5 @@ /** - * Flow types — framework-agnostic (ported from rfunds-online, @xyflow removed). + * Flow types — framework-agnostic (ported from rflows-online, @xyflow removed). */ export interface IntegrationSource { diff --git a/modules/rflows/local-first-client.ts b/modules/rflows/local-first-client.ts index ed1ece3..e251616 100644 --- a/modules/rflows/local-first-client.ts +++ b/modules/rflows/local-first-client.ts @@ -1,5 +1,5 @@ /** - * rFunds Local-First Client + * rFlows Local-First Client * * Wraps the shared local-first stack for space-flow associations. * Actual flow logic stays in the external payment-flow service. @@ -11,10 +11,10 @@ import type { DocumentId } from '../../shared/local-first/document'; import { EncryptedDocStore } from '../../shared/local-first/storage'; import { DocSyncManager } from '../../shared/local-first/sync'; import { DocCrypto } from '../../shared/local-first/crypto'; -import { fundsSchema, fundsDocId } from './schemas'; -import type { FundsDoc, SpaceFlow } from './schemas'; +import { flowsSchema, flowsDocId } from './schemas'; +import type { FlowsDoc, SpaceFlow } from './schemas'; -export class FundsLocalFirstClient { +export class FlowsLocalFirstClient { #space: string; #documents: DocumentManager; #store: EncryptedDocStore; @@ -29,7 +29,7 @@ export class FundsLocalFirstClient { documents: this.#documents, store: this.#store, }); - this.#documents.registerSchema(fundsSchema); + this.#documents.registerSchema(flowsSchema); } get isConnected(): boolean { return this.#sync.isConnected; } @@ -37,53 +37,53 @@ export class FundsLocalFirstClient { async init(): Promise { if (this.#initialized) return; await this.#store.open(); - const cachedIds = await this.#store.listByModule('funds', 'flows'); + const cachedIds = await this.#store.listByModule('flows', 'data'); const cached = await this.#store.loadMany(cachedIds); for (const [docId, binary] of cached) { - this.#documents.open(docId, fundsSchema, binary); + this.#documents.open(docId, flowsSchema, binary); } await this.#sync.preloadSyncStates(cachedIds); const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${proto}//${location.host}/ws/${this.#space}`; - try { await this.#sync.connect(wsUrl, this.#space); } catch { console.warn('[FundsClient] Working offline'); } + try { await this.#sync.connect(wsUrl, this.#space); } catch { console.warn('[FlowsClient] Working offline'); } this.#initialized = true; } - async subscribe(): Promise { - const docId = fundsDocId(this.#space) as DocumentId; - let doc = this.#documents.get(docId); + async subscribe(): Promise { + const docId = flowsDocId(this.#space) as DocumentId; + let doc = this.#documents.get(docId); if (!doc) { const binary = await this.#store.load(docId); doc = binary - ? this.#documents.open(docId, fundsSchema, binary) - : this.#documents.open(docId, fundsSchema); + ? this.#documents.open(docId, flowsSchema, binary) + : this.#documents.open(docId, flowsSchema); } await this.#sync.subscribe([docId]); return doc ?? null; } - getFlows(): FundsDoc | undefined { - return this.#documents.get(fundsDocId(this.#space) as DocumentId); + getFlows(): FlowsDoc | undefined { + return this.#documents.get(flowsDocId(this.#space) as DocumentId); } addSpaceFlow(flow: SpaceFlow): void { - const docId = fundsDocId(this.#space) as DocumentId; - this.#sync.change(docId, `Add flow ${flow.flowId}`, (d) => { + const docId = flowsDocId(this.#space) as DocumentId; + this.#sync.change(docId, `Add flow ${flow.flowId}`, (d) => { d.spaceFlows[flow.id] = flow; }); } removeSpaceFlow(flowId: string): void { - const docId = fundsDocId(this.#space) as DocumentId; - this.#sync.change(docId, `Remove flow ${flowId}`, (d) => { + const docId = flowsDocId(this.#space) as DocumentId; + this.#sync.change(docId, `Remove flow ${flowId}`, (d) => { for (const [id, sf] of Object.entries(d.spaceFlows)) { if (sf.flowId === flowId) delete d.spaceFlows[id]; } }); } - onChange(cb: (doc: FundsDoc) => void): () => void { - return this.#sync.onChange(fundsDocId(this.#space) as DocumentId, cb as (doc: any) => void); + onChange(cb: (doc: FlowsDoc) => void): () => void { + return this.#sync.onChange(flowsDocId(this.#space) as DocumentId, cb as (doc: any) => void); } onConnect(cb: () => void): () => void { return this.#sync.onConnect(cb); } diff --git a/modules/rflows/mod.ts b/modules/rflows/mod.ts index 6e1935d..c265fbf 100644 --- a/modules/rflows/mod.ts +++ b/modules/rflows/mod.ts @@ -1,7 +1,7 @@ /** - * Funds module — budget flows, river visualization, and treasury management. + * Flows module — budget flows, river visualization, and treasury management. * - * Proxies flow-service API calls and serves the BudgetRiver visualization. + * Proxies flow-service API calls and serves the FlowRiver visualization. */ import { Hono } from "hono"; @@ -12,18 +12,18 @@ import { getModuleInfoList } from "../../shared/module"; import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server"; import { renderLanding } from "./landing"; import type { SyncServer } from '../../server/local-first/sync-server'; -import { fundsSchema, fundsDocId, type FundsDoc, type SpaceFlow } from './schemas'; +import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow } from './schemas'; let _syncServer: SyncServer | null = null; const FLOW_SERVICE_URL = process.env.FLOW_SERVICE_URL || "http://payment-flow:3010"; -function ensureDoc(space: string): FundsDoc { - const docId = fundsDocId(space); - let doc = _syncServer!.getDoc(docId); +function ensureDoc(space: string): FlowsDoc { + const docId = flowsDocId(space); + let doc = _syncServer!.getDoc(docId); if (!doc) { - doc = Automerge.change(Automerge.init(), 'init', (d) => { - const init = fundsSchema.init(); + doc = Automerge.change(Automerge.init(), 'init', (d) => { + const init = flowsSchema.init(); d.meta = init.meta; d.meta.spaceSlug = space; d.spaceFlows = {}; @@ -224,9 +224,9 @@ routes.post("/api/space-flows", async (c) => { const { space, flowId } = await c.req.json(); if (!space || !flowId) return c.json({ error: "space and flowId required" }, 400); - const docId = fundsDocId(space); + const docId = flowsDocId(space); ensureDoc(space); - _syncServer!.changeDoc(docId, 'add space flow', (d) => { + _syncServer!.changeDoc(docId, 'add space flow', (d) => { const key = `${space}:${flowId}`; if (!d.spaceFlows[key]) { d.spaceFlows[key] = { id: key, spaceSlug: space, flowId, addedBy: claims.sub, createdAt: Date.now() }; @@ -245,12 +245,12 @@ routes.delete("/api/space-flows/:flowId", async (c) => { const space = c.req.query("space") || ""; if (!space) return c.json({ error: "space query param required" }, 400); - const docId = fundsDocId(space); - const doc = _syncServer!.getDoc(docId); + const docId = flowsDocId(space); + const doc = _syncServer!.getDoc(docId); if (doc) { const key = `${space}:${flowId}`; if (doc.spaceFlows[key]) { - _syncServer!.changeDoc(docId, 'remove space flow', (d) => { + _syncServer!.changeDoc(docId, 'remove space flow', (d) => { delete d.spaceFlows[key]; }); } @@ -260,25 +260,25 @@ routes.delete("/api/space-flows/:flowId", async (c) => { // ─── Page routes ──────────────────────────────────────── -const fundsScripts = ` +const flowsScripts = ` - - `; + + `; -const fundsStyles = ``; +const flowsStyles = ``; // Landing page (also serves demo via centralized /demo → space="demo" rewrite) routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; return c.html(renderShell({ - title: `${spaceSlug} — Funds | rSpace`, - moduleId: "rfunds", + title: `${spaceSlug} — Flows | rSpace`, + moduleId: "rflows", spaceSlug, modules: getModuleInfoList(), theme: "dark", - body: ``, - scripts: fundsScripts, - styles: fundsStyles, + body: ``, + scripts: flowsScripts, + styles: flowsStyles, })); }); @@ -287,54 +287,54 @@ routes.get("/flow/:flowId", (c) => { const spaceSlug = c.req.param("space") || "demo"; const flowId = c.req.param("flowId"); return c.html(renderShell({ - title: `Flow — rFunds | rSpace`, - moduleId: "rfunds", + title: `Flow — rFlows | rSpace`, + moduleId: "rflows", spaceSlug, modules: getModuleInfoList(), theme: "dark", - styles: fundsStyles, - body: ``, - scripts: fundsScripts, + styles: flowsStyles, + body: ``, + scripts: flowsScripts, })); }); // ── Seed template data ── -function seedTemplateFunds(space: string) { +function seedTemplateFlows(space: string) { if (!_syncServer) return; const doc = ensureDoc(space); if (Object.keys(doc.spaceFlows).length > 0) return; - const docId = fundsDocId(space); + const docId = flowsDocId(space); const now = Date.now(); const flowId = crypto.randomUUID(); // Create a SpaceFlow entry pointing to "demo" — the frontend // already renders demoNodes from presets.ts in demo mode. - _syncServer.changeDoc(docId, 'seed template flow', (d) => { + _syncServer.changeDoc(docId, 'seed template flow', (d) => { d.spaceFlows[flowId] = { id: flowId, spaceSlug: space, flowId: 'demo', addedBy: 'did:demo:seed', createdAt: now, }; }); - console.log(`[Funds] Template seeded for "${space}": 1 demo flow association`); + console.log(`[Flows] Template seeded for "${space}": 1 demo flow association`); } -export const fundsModule: RSpaceModule = { - id: "rfunds", - name: "rFunds", +export const flowsModule: RSpaceModule = { + id: "rflows", + name: "rFlows", icon: "🌊", description: "Budget flows, river visualization, and treasury management", scoping: { defaultScope: 'space', userConfigurable: false }, - docSchemas: [{ pattern: '{space}:funds:flows', description: 'Space flow associations', init: fundsSchema.init }], + docSchemas: [{ pattern: '{space}:flows:data', description: 'Space flow associations', init: flowsSchema.init }], routes, landingPage: renderLanding, - seedTemplate: seedTemplateFunds, + seedTemplate: seedTemplateFlows, async onInit(ctx) { _syncServer = ctx.syncServer; }, - standaloneDomain: "rfunds.online", + standaloneDomain: "rflows.online", feeds: [ { id: "treasury-flows", diff --git a/modules/rflows/schemas.ts b/modules/rflows/schemas.ts index 6e0f49c..2aa2102 100644 --- a/modules/rflows/schemas.ts +++ b/modules/rflows/schemas.ts @@ -1,8 +1,8 @@ /** - * rFunds Automerge document schemas. + * rFlows Automerge document schemas. * * Granularity: one Automerge document per space (flow associations). - * DocId format: {space}:funds:flows + * DocId format: {space}:flows:data * * Actual flow logic stays in the external payment-flow service. * This doc tracks which flows are associated with which spaces. @@ -20,7 +20,7 @@ export interface SpaceFlow { createdAt: number; } -export interface FundsDoc { +export interface FlowsDoc { meta: { module: string; collection: string; @@ -33,14 +33,14 @@ export interface FundsDoc { // ── Schema registration ── -export const fundsSchema: DocSchema = { - module: 'funds', - collection: 'flows', +export const flowsSchema: DocSchema = { + module: 'flows', + collection: 'data', version: 1, - init: (): FundsDoc => ({ + init: (): FlowsDoc => ({ meta: { - module: 'funds', - collection: 'flows', + module: 'flows', + collection: 'data', version: 1, spaceSlug: '', createdAt: Date.now(), @@ -51,6 +51,6 @@ export const fundsSchema: DocSchema = { // ── Helpers ── -export function fundsDocId(space: string) { - return `${space}:funds:flows` as const; +export function flowsDocId(space: string) { + return `${space}:flows:data` as const; } diff --git a/modules/rnotes/components/folk-notes-app.ts b/modules/rnotes/components/folk-notes-app.ts index 1243697..a45c239 100644 --- a/modules/rnotes/components/folk-notes-app.ts +++ b/modules/rnotes/components/folk-notes-app.ts @@ -157,7 +157,7 @@ class FolkNotesApp extends HTMLElement { Accommodation: EUR 1200 (30%) Activities: EUR 1000 (25%) Food: EUR 600 (15%) -Gear: EUR 400 (10%)

Maya is tracking expenses in rFunds. Current spend: EUR 1,203.

`, +Gear: EUR 400 (10%)

Maya is tracking expenses in rFlows. Current spend: EUR 1,203.

`, content_plain: "Pre-trip preparation checklist covering flights, transfers, travel documents, and budget overview for the Alpine Explorer 2026 trip.", content_format: 'html', type: "NOTE", tags: ["planning", "budget", "transport"], is_pinned: true, diff --git a/modules/rnotes/mod.ts b/modules/rnotes/mod.ts index b3e45ac..584a074 100644 --- a/modules/rnotes/mod.ts +++ b/modules/rnotes/mod.ts @@ -171,7 +171,7 @@ function seedDemoIfEmpty(space: string) { }, { nbId: nb1Id, nbDocId: nb1DocId, title: "Revenue Sharing Model", - content: "## Current Split\n\n| Recipient | Share | Rationale |\n|-----------|-------|-----------|\n| Provider | 50% | Covers materials, labor, shipping |\n| Creator | 35% | Design and creative work |\n| Community | 15% | Platform maintenance, commons fund |\n\n## Enoughness Thresholds\n\nOnce a funnel reaches its sufficient threshold, surplus flows to the next highest-need funnel. This prevents accumulation and keeps resources flowing.\n\n## Implementation\n\nrFunds Flow Service handles deposits from rCart. Each order total is routed through the configured flow → funnel → overflow splits.", + content: "## Current Split\n\n| Recipient | Share | Rationale |\n|-----------|-------|-----------|\n| Provider | 50% | Covers materials, labor, shipping |\n| Creator | 35% | Design and creative work |\n| Community | 15% | Platform maintenance, commons fund |\n\n## Enoughness Thresholds\n\nOnce a funnel reaches its sufficient threshold, surplus flows to the next highest-need funnel. This prevents accumulation and keeps resources flowing.\n\n## Implementation\n\nrFlows Flow Service handles deposits from rCart. Each order total is routed through the configured flow → funnel → overflow splits.", tags: ["cosmolocal", "governance"], }, { @@ -181,7 +181,7 @@ function seedDemoIfEmpty(space: string) { }, { nbId: nb2Id, nbDocId: nb2DocId, title: "Weekly Standup — Feb 15, 2026", - content: "## Attendees\n\nAlice, Bob, Carol\n\n## Updates\n\n**Alice**: Finished EncryptID guardian recovery flow. 2-of-3 guardian approval working. Next: device linking via QR code.\n\n**Bob**: Provider registry now has 6 printers globally. Working on proximity search with earthdistance extension.\n\n**Carol**: rFunds river visualization deployed. Enoughness layer showing golden glow on sufficient funnels.\n\n## Action Items\n\n- [ ] Alice: Document guardian recovery API endpoints\n- [ ] Bob: Add turnaround time estimates to provider matching\n- [ ] Carol: Add demo mode to river view with mock data", + content: "## Attendees\n\nAlice, Bob, Carol\n\n## Updates\n\n**Alice**: Finished EncryptID guardian recovery flow. 2-of-3 guardian approval working. Next: device linking via QR code.\n\n**Bob**: Provider registry now has 6 printers globally. Working on proximity search with earthdistance extension.\n\n**Carol**: rFlows river visualization deployed. Enoughness layer showing golden glow on sufficient funnels.\n\n## Action Items\n\n- [ ] Alice: Document guardian recovery API endpoints\n- [ ] Bob: Add turnaround time estimates to provider matching\n- [ ] Carol: Add demo mode to river view with mock data", tags: ["standup"], }, { diff --git a/modules/rtrips/mod.ts b/modules/rtrips/mod.ts index 04cd377..3deec1d 100644 --- a/modules/rtrips/mod.ts +++ b/modules/rtrips/mod.ts @@ -483,13 +483,13 @@ function seedTemplateTrips(space: string) { d.destinations[dest2Id] = { id: dest2Id, tripId, name: 'Athens', country: 'Greece', lat: 37.9838, lng: 23.7275, arrivalDate: '2026-04-16', departureDate: '2026-04-18', - notes: 'Commons Fest workshop — present rFunds river visualization.', sortOrder: 1, createdAt: now, + notes: 'Commons Fest workshop — present rFlows river visualization.', sortOrder: 1, createdAt: now, }; d.itinerary = {}; const itin = [ { destId: dest1Id, title: 'Maker Space Visit', category: 'meeting', date: '2026-04-14', start: '10:00', end: '13:00', notes: 'Tour facilities, discuss print capabilities' }, { destId: dest1Id, title: 'Prototype Session', category: 'workshop', date: '2026-04-15', start: '09:00', end: '17:00', notes: 'Full-day sprint on cosmolocal order flow' }, - { destId: dest2Id, title: 'Commons Fest Presentation', category: 'conference', date: '2026-04-17', start: '14:00', end: '16:00', notes: 'Present rFunds + rVote governance tools' }, + { destId: dest2Id, title: 'Commons Fest Presentation', category: 'conference', date: '2026-04-17', start: '14:00', end: '16:00', notes: 'Present rFlows + rVote governance tools' }, ]; for (let i = 0; i < itin.length; i++) { const iId = crypto.randomUUID(); diff --git a/modules/rwallet/landing.ts b/modules/rwallet/landing.ts index b584ea1..142c8be 100644 --- a/modules/rwallet/landing.ts +++ b/modules/rwallet/landing.ts @@ -90,7 +90,7 @@ export function renderLanding(): string {
📈
-

rFunds

+

rFlows

Overlay budget categories on your wallet data to see where funds are allocated and how they flow.

diff --git a/server/seed-demo.ts b/server/seed-demo.ts index e5d44da..5b8d9ce 100644 --- a/server/seed-demo.ts +++ b/server/seed-demo.ts @@ -245,7 +245,7 @@ const DEMO_SHAPES: Record[] = [ store: "StarRent.eu", }, - // ─── rFunds: Expenses ─────────────────────────────────────── + // ─── rFlows: Expenses ─────────────────────────────────────── { id: "demo-expense-shuttle", type: "demo-expense", @@ -307,7 +307,7 @@ const DEMO_SHAPES: Record[] = [ date: "2026-07-13", }, - // ─── rFunds: Budget ───────────────────────────────────────── + // ─── rFlows: Budget ───────────────────────────────────────── { id: "demo-budget-trip", type: "folk-budget", diff --git a/server/seed-template.ts b/server/seed-template.ts index 1833d81..6757df4 100644 --- a/server/seed-template.ts +++ b/server/seed-template.ts @@ -113,7 +113,7 @@ const TEMPLATE_SHAPES: Record[] = [ store: "", }, - // ─── rFunds: Budget ───────────────────────────────────────── + // ─── rFlows: Budget ───────────────────────────────────────── { id: "tmpl-budget", type: "folk-budget", @@ -129,7 +129,7 @@ const TEMPLATE_SHAPES: Record[] = [ ], }, - // ─── rFunds: Expense ──────────────────────────────────────── + // ─── rFlows: Expense ──────────────────────────────────────── { id: "tmpl-expense", type: "demo-expense", diff --git a/shared/components/rstack-app-switcher.ts b/shared/components/rstack-app-switcher.ts index bc525e4..5aa8d7e 100644 --- a/shared/components/rstack-app-switcher.ts +++ b/shared/components/rstack-app-switcher.ts @@ -38,7 +38,7 @@ const MODULE_BADGES: Record = { rchoices: { badge: "rCo", color: "#f0abfc" }, // fuchsia-300 rvote: { badge: "rV", color: "#c4b5fd" }, // violet-300 // Funding & Commerce - rfunds: { badge: "rF", color: "#bef264" }, // lime-300 + rflows: { badge: "rFl", color: "#bef264" }, // lime-300 rwallet: { badge: "rW", color: "#fde047" }, // yellow-300 rcart: { badge: "rCt", color: "#fdba74" }, // orange-300 rauctions: { badge: "rA", color: "#fca5a5" }, // red-300 @@ -75,7 +75,7 @@ const MODULE_CATEGORIES: Record = { rforum: "Communicating", rchoices: "Deciding", rvote: "Deciding", - rfunds: "Funding & Commerce", + rflows: "Funding & Commerce", rwallet: "Funding & Commerce", rcart: "Funding & Commerce", rauctions: "Funding & Commerce", diff --git a/shared/components/rstack-tab-bar.ts b/shared/components/rstack-tab-bar.ts index 3d7c1d6..a2f8979 100644 --- a/shared/components/rstack-tab-bar.ts +++ b/shared/components/rstack-tab-bar.ts @@ -39,7 +39,7 @@ const MODULE_BADGES: Record = { rforum: { badge: "rFo", color: "#fcd34d" }, rchoices: { badge: "rCo", color: "#f0abfc" }, rvote: { badge: "rV", color: "#c4b5fd" }, - rfunds: { badge: "rF", color: "#bef264" }, + rflows: { badge: "rFl", color: "#bef264" }, rwallet: { badge: "rW", color: "#fde047" }, rcart: { badge: "rCt", color: "#fdba74" }, rauctions: { badge: "rA", color: "#fca5a5" }, @@ -60,7 +60,7 @@ const MODULE_CATEGORIES: Record = { rcal: "Planning", rtrips: "Planning", rmaps: "Planning", rchats: "Communicating", rinbox: "Communicating", rmail: "Communicating", rforum: "Communicating", rchoices: "Deciding", rvote: "Deciding", - rfunds: "Funding & Commerce", rwallet: "Funding & Commerce", rcart: "Funding & Commerce", rauctions: "Funding & Commerce", + rflows: "Funding & Commerce", rwallet: "Funding & Commerce", rcart: "Funding & Commerce", rauctions: "Funding & Commerce", rphotos: "Sharing", rnetwork: "Sharing", rsocials: "Sharing", rfiles: "Sharing", rbooks: "Sharing", rdata: "Observing", rwork: "Work & Productivity", diff --git a/vite.config.ts b/vite.config.ts index 2b5f65d..53e3994 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -214,51 +214,51 @@ export default defineConfig({ resolve(__dirname, "dist/modules/rchoices/choices.css"), ); - // Build funds module components - const fundsAlias = { - "../lib/types": resolve(__dirname, "modules/rfunds/lib/types.ts"), - "../lib/simulation": resolve(__dirname, "modules/rfunds/lib/simulation.ts"), - "../lib/presets": resolve(__dirname, "modules/rfunds/lib/presets.ts"), - "../lib/map-flow": resolve(__dirname, "modules/rfunds/lib/map-flow.ts"), + // Build flows module components + const flowsAlias = { + "../lib/types": resolve(__dirname, "modules/rflows/lib/types.ts"), + "../lib/simulation": resolve(__dirname, "modules/rflows/lib/simulation.ts"), + "../lib/presets": resolve(__dirname, "modules/rflows/lib/presets.ts"), + "../lib/map-flow": resolve(__dirname, "modules/rflows/lib/map-flow.ts"), }; await build({ configFile: false, - root: resolve(__dirname, "modules/rfunds/components"), - resolve: { alias: fundsAlias }, + root: resolve(__dirname, "modules/rflows/components"), + resolve: { alias: flowsAlias }, build: { emptyOutDir: false, - outDir: resolve(__dirname, "dist/modules/rfunds"), + outDir: resolve(__dirname, "dist/modules/rflows"), lib: { - entry: resolve(__dirname, "modules/rfunds/components/folk-budget-river.ts"), + entry: resolve(__dirname, "modules/rflows/components/folk-flow-river.ts"), formats: ["es"], - fileName: () => "folk-budget-river.js", + fileName: () => "folk-flow-river.js", }, - rollupOptions: { output: { entryFileNames: "folk-budget-river.js" } }, + rollupOptions: { output: { entryFileNames: "folk-flow-river.js" } }, }, }); await build({ configFile: false, - root: resolve(__dirname, "modules/rfunds/components"), - resolve: { alias: fundsAlias }, + root: resolve(__dirname, "modules/rflows/components"), + resolve: { alias: flowsAlias }, build: { emptyOutDir: false, - outDir: resolve(__dirname, "dist/modules/rfunds"), + outDir: resolve(__dirname, "dist/modules/rflows"), lib: { - entry: resolve(__dirname, "modules/rfunds/components/folk-funds-app.ts"), + entry: resolve(__dirname, "modules/rflows/components/folk-flows-app.ts"), formats: ["es"], - fileName: () => "folk-funds-app.js", + fileName: () => "folk-flows-app.js", }, - rollupOptions: { output: { entryFileNames: "folk-funds-app.js" } }, + rollupOptions: { output: { entryFileNames: "folk-flows-app.js" } }, }, }); - // Copy funds CSS - mkdirSync(resolve(__dirname, "dist/modules/rfunds"), { recursive: true }); + // Copy flows CSS + mkdirSync(resolve(__dirname, "dist/modules/rflows"), { recursive: true }); copyFileSync( - resolve(__dirname, "modules/rfunds/components/funds.css"), - resolve(__dirname, "dist/modules/rfunds/funds.css"), + resolve(__dirname, "modules/rflows/components/flows.css"), + resolve(__dirname, "dist/modules/rflows/flows.css"), ); // Build files module component @@ -797,7 +797,7 @@ export default defineConfig({ }); // Build demo scripts for each module that has one - const demoModules = ["cart", "vote", "funds", "notes", "cal", "tube", "trips"]; + const demoModules = ["cart", "vote", "flows", "notes", "cal", "tube", "trips"]; for (const mod of demoModules) { const dir = `r${mod}`; const demoEntry = resolve(__dirname, `modules/${dir}/components/${mod}-demo.ts`); diff --git a/website/canvas.html b/website/canvas.html index 646860a..2bb097c 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1803,7 +1803,7 @@ - +
@@ -3733,7 +3733,7 @@ { btnId: "embed-forum", moduleId: "rforum" }, { btnId: "embed-inbox", moduleId: "rinbox" }, { btnId: "embed-tube", moduleId: "rtube" }, - { btnId: "embed-funds", moduleId: "rfunds" }, + { btnId: "embed-flows", moduleId: "rflows" }, { btnId: "embed-wallet", moduleId: "rwallet" }, { btnId: "embed-vote", moduleId: "rvote" }, { btnId: "embed-cart", moduleId: "rcart" }, diff --git a/website/index.html b/website/index.html index c7be1b9..81481af 100644 --- a/website/index.html +++ b/website/index.html @@ -359,7 +359,7 @@ Every community rSpace comes with a full suite of interoperable tools — voting, budgets, maps, files, notes, and more — all sharing the same EncryptID session. Sign in once on rSpace and you're - already authenticated on rVote, rFunds, rFiles, and every other tool your + already authenticated on rVote, rFlows, rFiles, and every other tool your community uses. No separate accounts, no OAuth redirects, no third-party identity providers. Your community, your identity, your data.

@@ -429,7 +429,7 @@ v ┌─────────── rSpace CRDT Sync Layer ───────────┐ | | - | rVote rFunds rFiles rNotes rMaps ... | + | rVote rFlows rFiles rNotes rMaps ... | | | | | | | | | └───────┴───────┴───────┴───────┘ | | shared community data graph | @@ -444,7 +444,7 @@

Your data, connected across tools

- A budget created in rFunds can reference a vote + A budget created in rFlows can reference a vote from rVote. A map pin in rMaps can link to files in rFiles and notes in rNotes. Because all r-Ecosystem tools share the same @@ -473,7 +473,7 @@ 📁 rFiles 📝 rNotes ✈ rTrips - 💸 rFunds + 💸 rFlows 🕸️ rNetwork 🛒 rCart 🎬 rTube diff --git a/website/public/landing.html b/website/public/landing.html index f5826be..d1c4fbe 100644 --- a/website/public/landing.html +++ b/website/public/landing.html @@ -1,13 +1,13 @@ -rSpace.online - Reclaim Your Space on the New Web

rSpace logo
Welcome to

rSpace.online

Remember back when the internet was cool?

We may not have Myspace anymore, but we have

(ou)rSpace

(and it's online)

Build digital spaces to collaborate on improving your physical world. Local-first, zero knowledge data privacy, outside the walls of big tech.

Zero-Knowledge Data PrivacySelf-InfrastructuredLocal-First

Shared Digital Spaces for Collaboration

Everything you need to build thriving communities outside big tech's surveillance apparatus

Shared Funds

Allocate and manage community resources through transparent, customizable controls.

Secure Messaging

End-to-end encrypted communication channels for your community. Nobody can read your messages, not even us.

File Sharing

Share files and create collaborative views over data with localized, zero-knowledge storage.

Delegated Authority

Manage group permissions and authority structures democratically. Your space, your rules.

Interactive Dashboards

Multidimensional views of your data powered by folkjs - HTML as a computing substrate.

Data Privacy

Zero-knowledge architecture ensures your data never exists unencrypted outside your control.

EncryptID

One secure, local-first identity across every tool in your community's rSpace. No passwords. No cloud accounts. Your keys never leave your device.

Passkey Login

Hardware-backed biometric auth. Phishing-resistant by design.

Local-First

Cryptographic keys are derived and stored on your device, never uploaded.

One Login, All Apps

Authenticate once and access every r-Ecosystem tool seamlessly.

Secure by default, not by opt-in

EncryptID uses WebAuthn passkeys as the root of trust — the same standard behind Face ID and fingerprint unlock. Your identity is bound to your device's secure hardware, so there are no passwords to leak, phish, or forget. End-to-end encryption keys are derived locally via HKDF, meaning the server never sees your private keys. If you lose your device, social recovery lets trusted guardians help you regain access without seed phrases or centralized reset flows.

A common login for your community's toolkit

Every community rSpace comes with a full suite of interoperable tools — voting, budgets, maps, files, notes, and more — all sharing the same EncryptID session. Sign in once on rSpace and you're already authenticated on rVote, rFunds, rFiles, and every other tool your community uses. No separate accounts, no OAuth redirects, no third-party identity providers. Your community, your identity, your data.

Offline-First, Always Available

rSpace works without an internet connection. Edit your canvas on a plane, in a field, or underground — your changes merge automatically when you're back online. No sync buttons, no conflict dialogs.

Local Persistence

Your canvas is cached in the browser. Refresh the page — it loads instantly, even offline.

Auto-Merge

Automerge CRDTs resolve conflicts automatically. Multiple people can edit the same canvas offline and merge without data loss.

Incremental Sync

Only new changes are transferred on reconnect — not the whole document. Fast even on slow connections.

How it works

Every rSpace canvas is an Automerge CRDT document stored locally in your browser's IndexedDB. When you open a canvas, it renders from the local cache first — no waiting for the server. Edits you make are saved locally and synced to the server via WebSocket when a connection is available. If you go offline, the app keeps working: a Service Worker serves the app shell from cache, and your changes accumulate in the local CRDT document. When connectivity returns, Automerge's incremental sync protocol reconciles your changes with everyone else's — conflict-free, automatically. No manual merge, no "which version do you want to keep?" dialogs.

The Internet as It Was Always Meant to Be

The legacy internet has become a collection of walled gardens controlled by a handful of corporations. Your data is commodified. Your attention is sold. Your privacy is an illusion.

rSpace is built on a simple principle: technology should serve people, not profit margins.

We're bringing back the spirit of the early web - when MySpace let you express yourself, when communities formed around shared purpose, when the internet felt like it belonged to everyone.

Local-First Principles

Your data lives on your devices. Work offline. Sync when connected. No central servers controlling your access or mining your information.

Communities of Purpose

Build spaces around what matters - not what drives engagement metrics. Organize, collaborate, and govern democratically.

Zero Knowledge Privacy

End-to-end encrypted by default. Your data never exists unencrypted outside your control. Surveillance capitalism has no place here.

Built Different

A new substrate for digital collaboration

01

Built on folkjs

HTML as a computing substrate. Interactive, reactive interfaces using familiar web technologies - no complex frameworks required.

02

Local-First Storage

Your data lives on your device. Distributed sync protocols keep your rSpace updated across the mesh without centralized servers.

03

Zero-Knowledge Architecture

All encryption happens on your device. Share selectively through cryptographic proofs. Nobody can access what they shouldn't see.

Powered by folkjs - bringing the internet back to what it was always meant to be

A basic HTML computing substrate for building truly interactive, local-first applications

Interoperable by Design

Data flows between your community's tools because they share a common foundation: the same identity, the same real-time sync layer, and the same local-first architecture.

  EncryptID (identity)
+rSpace.online - Reclaim Your Space on the New Web
rSpace logo
Welcome to

rSpace.online

Remember back when the internet was cool?

We may not have Myspace anymore, but we have

(ou)rSpace

(and it's online)

Build digital spaces to collaborate on improving your physical world. Local-first, zero knowledge data privacy, outside the walls of big tech.

Zero-Knowledge Data PrivacySelf-InfrastructuredLocal-First

Shared Digital Spaces for Collaboration

Everything you need to build thriving communities outside big tech's surveillance apparatus

Shared Funds

Allocate and manage community resources through transparent, customizable controls.

Secure Messaging

End-to-end encrypted communication channels for your community. Nobody can read your messages, not even us.

File Sharing

Share files and create collaborative views over data with localized, zero-knowledge storage.

Delegated Authority

Manage group permissions and authority structures democratically. Your space, your rules.

Interactive Dashboards

Multidimensional views of your data powered by folkjs - HTML as a computing substrate.

Data Privacy

Zero-knowledge architecture ensures your data never exists unencrypted outside your control.

EncryptID

One secure, local-first identity across every tool in your community's rSpace. No passwords. No cloud accounts. Your keys never leave your device.

Passkey Login

Hardware-backed biometric auth. Phishing-resistant by design.

Local-First

Cryptographic keys are derived and stored on your device, never uploaded.

One Login, All Apps

Authenticate once and access every r-Ecosystem tool seamlessly.

Secure by default, not by opt-in

EncryptID uses WebAuthn passkeys as the root of trust — the same standard behind Face ID and fingerprint unlock. Your identity is bound to your device's secure hardware, so there are no passwords to leak, phish, or forget. End-to-end encryption keys are derived locally via HKDF, meaning the server never sees your private keys. If you lose your device, social recovery lets trusted guardians help you regain access without seed phrases or centralized reset flows.

A common login for your community's toolkit

Every community rSpace comes with a full suite of interoperable tools — voting, budgets, maps, files, notes, and more — all sharing the same EncryptID session. Sign in once on rSpace and you're already authenticated on rVote, rFlows, rFiles, and every other tool your community uses. No separate accounts, no OAuth redirects, no third-party identity providers. Your community, your identity, your data.

Offline-First, Always Available

rSpace works without an internet connection. Edit your canvas on a plane, in a field, or underground — your changes merge automatically when you're back online. No sync buttons, no conflict dialogs.

Local Persistence

Your canvas is cached in the browser. Refresh the page — it loads instantly, even offline.

Auto-Merge

Automerge CRDTs resolve conflicts automatically. Multiple people can edit the same canvas offline and merge without data loss.

Incremental Sync

Only new changes are transferred on reconnect — not the whole document. Fast even on slow connections.

How it works

Every rSpace canvas is an Automerge CRDT document stored locally in your browser's IndexedDB. When you open a canvas, it renders from the local cache first — no waiting for the server. Edits you make are saved locally and synced to the server via WebSocket when a connection is available. If you go offline, the app keeps working: a Service Worker serves the app shell from cache, and your changes accumulate in the local CRDT document. When connectivity returns, Automerge's incremental sync protocol reconciles your changes with everyone else's — conflict-free, automatically. No manual merge, no "which version do you want to keep?" dialogs.

The Internet as It Was Always Meant to Be

The legacy internet has become a collection of walled gardens controlled by a handful of corporations. Your data is commodified. Your attention is sold. Your privacy is an illusion.

rSpace is built on a simple principle: technology should serve people, not profit margins.

We're bringing back the spirit of the early web - when MySpace let you express yourself, when communities formed around shared purpose, when the internet felt like it belonged to everyone.

Local-First Principles

Your data lives on your devices. Work offline. Sync when connected. No central servers controlling your access or mining your information.

Communities of Purpose

Build spaces around what matters - not what drives engagement metrics. Organize, collaborate, and govern democratically.

Zero Knowledge Privacy

End-to-end encrypted by default. Your data never exists unencrypted outside your control. Surveillance capitalism has no place here.

Built Different

A new substrate for digital collaboration

01

Built on folkjs

HTML as a computing substrate. Interactive, reactive interfaces using familiar web technologies - no complex frameworks required.

02

Local-First Storage

Your data lives on your device. Distributed sync protocols keep your rSpace updated across the mesh without centralized servers.

03

Zero-Knowledge Architecture

All encryption happens on your device. Share selectively through cryptographic proofs. Nobody can access what they shouldn't see.

Powered by folkjs - bringing the internet back to what it was always meant to be

A basic HTML computing substrate for building truly interactive, local-first applications

Interoperable by Design

Data flows between your community's tools because they share a common foundation: the same identity, the same real-time sync layer, and the same local-first architecture.

  EncryptID (identity)
        |
        v
   ┌─────────── rSpace CRDT Sync Layer ───────────┐
   |                                               |
-  |   rVote  rFunds  rFiles  rNotes  rMaps  ...   |
+  |   rVote  rFlows  rFiles  rNotes  rMaps  ...   |
   |     |       |       |       |       |         |
   |     └───────┴───────┴───────┴───────┘         |
   |         shared community data graph           |
   └───────────────────────────────────────────────┘
        |
        v
-  Your device (keys & data stay here)

Your data, connected across tools

A budget created in rFunds can reference a vote from rVote. A map pin in rMaps can link to files in rFiles and notes in rNotes. Because all r-Ecosystem tools share the same Automerge CRDT sync layer, data is interoperable without import/export steps or API integrations. Changes propagate in real-time across every tool and every collaborator — conflict-free.

No vendor lock-in, no data silos

Every piece of community data is stored as a local-first CRDT document that your community owns. There's no central server gating access and no proprietary format trapping your data. Export everything. Fork your community. Move between hosts. The r-Ecosystem is designed so that the community — not the platform — controls the data.

Shared Digital Spaces for Real World Impact

rSpaces aren't just for chatting online—they're tools for making your real neighborhood better.

The rSpace Ecosystem

rMaps

rMaps.online

Collaborative spatial mapping. Plot ideas, resources, and relationships on shared canvases.

rCart

rCart.online

Community commerce. Coordinate group purchases and share resources locally.

rCal

rCal.online

Shared calendars for communities. Coordinate events, meetings, and availability together.

rFunds

rFunds.online

Threshold-based flow funding. Design continuous funding flows with overflow routing and outcome tracking.

rFiles

rFiles.online

Shared file workspaces. Organize documents, media, and assets together in real time.

rWallet

rWallet.online

Community treasury management. Track shared resources, contributions, and funding flows.

rNotes

rNotes.online

Collaborative note-taking. Capture and organize ideas together in shared notebooks.

rVote

rVote.online

Democratic backlog prioritization. Quadratic proposal ranking lets communities surface the best ideas.

rTrips

rTrips.online

Plan adventures together. Coordinate group travel, shared itineraries, and trip logistics.

rTube

rTube.online

Community video sharing. Host and curate video content for your community.

rChats

rChats.online

Encrypted group messaging. Real-time channels for your community, end-to-end encrypted.

rForum

rForum.online

Community discussion forums. Threaded conversations for deeper, asynchronous dialogue.

rSwag

rSwag.online

Community merchandise on demand. Design custom stickers, shirts, and more with AI-powered tools.

rNetwork

rNetwork.online

Visualize community connections. Map relationships and networks across your ecosystem.

rInbox

rInbox.online

Community email and notifications. Unified inbox for all your rSpace communications.

rWork

rWork.online

Collaborative task management. Kanban boards and project pipelines for community projects.

Your space. Your community. Your rules.

Join the Movement

Part of a growing ecosystem building alternatives to surveillance capitalism

Ready to Reclaim Your Digital Space?

Join communities building the future of the web - one rSpace at a time

Stay Connected with rSpace

Subscribe for updates on real-time collaboration, spatial computing, and building connected futures.

No spam, unsubscribe anytime. We respect your privacy.

\ No newline at end of file + Your device (keys & data stay here)

Your data, connected across tools

A budget created in rFlows can reference a vote from rVote. A map pin in rMaps can link to files in rFiles and notes in rNotes. Because all r-Ecosystem tools share the same Automerge CRDT sync layer, data is interoperable without import/export steps or API integrations. Changes propagate in real-time across every tool and every collaborator — conflict-free.

No vendor lock-in, no data silos

Every piece of community data is stored as a local-first CRDT document that your community owns. There's no central server gating access and no proprietary format trapping your data. Export everything. Fork your community. Move between hosts. The r-Ecosystem is designed so that the community — not the platform — controls the data.

Shared Digital Spaces for Real World Impact

rSpaces aren't just for chatting online—they're tools for making your real neighborhood better.

The rSpace Ecosystem

rMaps

rMaps.online

Collaborative spatial mapping. Plot ideas, resources, and relationships on shared canvases.

rCart

rCart.online

Community commerce. Coordinate group purchases and share resources locally.

rCal

rCal.online

Shared calendars for communities. Coordinate events, meetings, and availability together.

rFlows

rFlows.online

Threshold-based flow funding. Design continuous funding flows with overflow routing and outcome tracking.

rFiles

rFiles.online

Shared file workspaces. Organize documents, media, and assets together in real time.

rWallet

rWallet.online

Community treasury management. Track shared resources, contributions, and funding flows.

rNotes

rNotes.online

Collaborative note-taking. Capture and organize ideas together in shared notebooks.

rVote

rVote.online

Democratic backlog prioritization. Quadratic proposal ranking lets communities surface the best ideas.

rTrips

rTrips.online

Plan adventures together. Coordinate group travel, shared itineraries, and trip logistics.

rTube

rTube.online

Community video sharing. Host and curate video content for your community.

rChats

rChats.online

Encrypted group messaging. Real-time channels for your community, end-to-end encrypted.

rForum

rForum.online

Community discussion forums. Threaded conversations for deeper, asynchronous dialogue.

rSwag

rSwag.online

Community merchandise on demand. Design custom stickers, shirts, and more with AI-powered tools.

rNetwork

rNetwork.online

Visualize community connections. Map relationships and networks across your ecosystem.

rInbox

rInbox.online

Community email and notifications. Unified inbox for all your rSpace communications.

rWork

rWork.online

Collaborative task management. Kanban boards and project pipelines for community projects.

Your space. Your community. Your rules.

Join the Movement

Part of a growing ecosystem building alternatives to surveillance capitalism

Ready to Reclaim Your Digital Space?

Join communities building the future of the web - one rSpace at a time

Stay Connected with rSpace

Subscribe for updates on real-time collaboration, spatial computing, and building connected futures.

No spam, unsubscribe anytime. We respect your privacy.

\ No newline at end of file