Merge branch 'dev'
This commit is contained in:
commit
453fd7e6b4
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const MODULE_META: Record<string, { badge: string; color: string; name: string;
|
|||
rforum: { badge: "rFo", color: "#fcd34d", name: "rForum", icon: "💬" },
|
||||
rinbox: { badge: "rI", color: "#a5b4fc", name: "rInbox", icon: "📧" },
|
||||
rtube: { badge: "rTu", color: "#f9a8d4", name: "rTube", icon: "🎬" },
|
||||
rfunds: { badge: "rF", color: "#bef264", name: "rFunds", icon: "🌊" },
|
||||
rflows: { badge: "rFl", color: "#bef264", name: "rFlows", icon: "🌊" },
|
||||
rwallet: { badge: "rW", color: "#fde047", name: "rWallet", icon: "💰" },
|
||||
rvote: { badge: "rV", color: "#c4b5fd", name: "rVote", icon: "🗳️" },
|
||||
rcart: { badge: "rCt", color: "#fdba74", name: "rCart", icon: "🛒" },
|
||||
|
|
@ -477,7 +477,7 @@ const WIDGET_API: Record<string, { path: string; transform: (data: any) => Widge
|
|||
};
|
||||
},
|
||||
},
|
||||
rfunds: {
|
||||
rflows: {
|
||||
path: "/api/flows",
|
||||
transform: (data) => {
|
||||
const flows = Array.isArray(data) ? data : [];
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function renderLanding(): string {
|
|||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">💰</div>
|
||||
<h3>Revenue Splits</h3>
|
||||
<p>Creator, community, and provider shares calculated automatically via rFunds. Transparent by default.</p>
|
||||
<p>Creator, community, and provider shares calculated automatically via rFlows. Transparent by default.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@ export function renderLanding(): string {
|
|||
</p>
|
||||
<ul class="rl-check-list">
|
||||
<li><strong>Provider matching</strong> — automatic routing by capability, location, and cost</li>
|
||||
<li><strong>Revenue splits</strong> — creator, community, and provider shares via rFunds</li>
|
||||
<li><strong>Revenue splits</strong> — creator, community, and provider shares via rFlows</li>
|
||||
<li><strong>Order tracking</strong> — real-time status from accepted to delivered</li>
|
||||
<li><strong>Volume pricing</strong> — automatic tier detection from pooled orders</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* <folk-budget-river> — animated SVG sankey river visualization.
|
||||
* <folk-flow-river> — 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);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* <folk-funds-app> — main rFunds application component.
|
||||
* <folk-flows-app> — 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; }
|
||||
</style>
|
||||
<link rel="stylesheet" href="${this.getCssPath()}">
|
||||
${this.error ? `<div class="funds-error">${this.esc(this.error)}</div>` : ""}
|
||||
${this.loading && this.view === "landing" ? '<div class="funds-loading">Loading...</div>' : ""}
|
||||
${this.error ? `<div class="flows-error">${this.esc(this.error)}</div>` : ""}
|
||||
${this.loading && this.view === "landing" ? '<div class="flows-loading">Loading...</div>' : ""}
|
||||
${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 `
|
||||
<div class="funds-landing">
|
||||
<div class="flows-landing">
|
||||
<div class="rapp-nav">
|
||||
<span class="rapp-nav__title">Flows</span>
|
||||
<div class="rapp-nav__actions">
|
||||
|
|
@ -239,53 +244,53 @@ class FolkFundsApp extends HTMLElement {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="funds-desc" style="color:#94a3b8;font-size:14px;line-height:1.6;max-width:600px;margin-bottom:24px">
|
||||
<div class="flows-desc" style="color:#94a3b8;font-size:14px;line-height:1.6;max-width:600px;margin-bottom:24px">
|
||||
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 <em style="color:#fbbf24;font-style:normal;font-weight:600">enough</em> before abundance cascades forward.
|
||||
</div>
|
||||
|
||||
<div class="funds-features">
|
||||
<div class="funds-features__grid">
|
||||
<div class="funds-features__card">
|
||||
<div class="funds-features__icon">💰</div>
|
||||
<div class="flows-features">
|
||||
<div class="flows-features__grid">
|
||||
<div class="flows-features__card">
|
||||
<div class="flows-features__icon">💰</div>
|
||||
<h3>Sources</h3>
|
||||
<p>Revenue streams split across funnels by configurable allocation percentages.</p>
|
||||
</div>
|
||||
<div class="funds-features__card">
|
||||
<div class="funds-features__icon">🏛</div>
|
||||
<div class="flows-features__card">
|
||||
<div class="flows-features__icon">🏛</div>
|
||||
<h3>Funnels</h3>
|
||||
<p>Budget buckets with min/max thresholds and sufficiency-based overflow cascading.</p>
|
||||
</div>
|
||||
<div class="funds-features__card">
|
||||
<div class="funds-features__icon">🎯</div>
|
||||
<div class="flows-features__card">
|
||||
<div class="flows-features__icon">🎯</div>
|
||||
<h3>Outcomes</h3>
|
||||
<p>Funding targets that receive spending allocations. Track progress toward each goal.</p>
|
||||
</div>
|
||||
<div class="funds-features__card">
|
||||
<div class="funds-features__icon">🌊</div>
|
||||
<div class="flows-features__card">
|
||||
<div class="flows-features__icon">🌊</div>
|
||||
<h3>River View</h3>
|
||||
<p>Animated sankey diagram showing live fund flows through your entire system.</p>
|
||||
</div>
|
||||
<div class="funds-features__card">
|
||||
<div class="funds-features__icon">✨</div>
|
||||
<div class="flows-features__card">
|
||||
<div class="flows-features__icon">✨</div>
|
||||
<h3>Enoughness</h3>
|
||||
<p>System-wide sufficiency scoring. Golden glow when funnels reach their threshold.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="funds-flows">
|
||||
<div class="funds-flows__header">
|
||||
<h2 class="funds-flows__heading">${authed ? `Flows in ${this.esc(this.space)}` : "Your Flows"}</h2>
|
||||
${authed ? `<span class="funds-flows__user">Signed in as ${this.esc(username || "")}</span>` : ""}
|
||||
<div class="flows-flows">
|
||||
<div class="flows-flows__header">
|
||||
<h2 class="flows-flows__heading">${authed ? `Flows in ${this.esc(this.space)}` : "Your Flows"}</h2>
|
||||
${authed ? `<span class="flows-flows__user">Signed in as ${this.esc(username || "")}</span>` : ""}
|
||||
</div>
|
||||
${this.flows.length > 0 ? `
|
||||
<div class="funds-flows__grid">
|
||||
<div class="flows-flows__grid">
|
||||
${this.flows.map((f) => this.renderFlowCard(f)).join("")}
|
||||
</div>
|
||||
` : `
|
||||
<div class="funds-flows__empty">
|
||||
<div class="flows-flows__empty">
|
||||
${authed
|
||||
? `<p>No flows in this space yet.</p>
|
||||
<p><a href="${this.esc(demoUrl)}">Explore the demo</a> or create your first flow.</p>`
|
||||
|
|
@ -295,25 +300,25 @@ class FolkFundsApp extends HTMLElement {
|
|||
`}
|
||||
</div>
|
||||
|
||||
<div class="funds-about">
|
||||
<h2 class="funds-about__heading">How TBFF Works</h2>
|
||||
<div class="funds-about__steps">
|
||||
<div class="funds-about__step">
|
||||
<div class="funds-about__step-num">1</div>
|
||||
<div class="flows-about">
|
||||
<h2 class="flows-about__heading">How TBFF Works</h2>
|
||||
<div class="flows-about__steps">
|
||||
<div class="flows-about__step">
|
||||
<div class="flows-about__step-num">1</div>
|
||||
<div>
|
||||
<h3>Define Sources</h3>
|
||||
<p>Add revenue streams — grants, donations, sales, or any recurring income — with allocation splits.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="funds-about__step">
|
||||
<div class="funds-about__step-num">2</div>
|
||||
<div class="flows-about__step">
|
||||
<div class="flows-about__step-num">2</div>
|
||||
<div>
|
||||
<h3>Configure Funnels</h3>
|
||||
<p>Set minimum, sufficient, and maximum thresholds. Overflow rules determine where surplus flows.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="funds-about__step">
|
||||
<div class="funds-about__step-num">3</div>
|
||||
<div class="flows-about__step">
|
||||
<div class="flows-about__step-num">3</div>
|
||||
<div>
|
||||
<h3>Track Outcomes</h3>
|
||||
<p>Funding targets receive allocations as funnels reach sufficiency. Watch the river flow in real time.</p>
|
||||
|
|
@ -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 `
|
||||
<a href="${this.esc(detailUrl)}" class="funds-flow-card" data-flow="${this.esc(f.id)}">
|
||||
<div class="funds-flow-card__name">${this.esc(f.name || f.label || f.id)}</div>
|
||||
${value ? `<div class="funds-flow-card__value">${value}</div>` : ""}
|
||||
<div class="funds-flow-card__meta">
|
||||
<a href="${this.esc(detailUrl)}" class="flows-flow-card" data-flow="${this.esc(f.id)}">
|
||||
<div class="flows-flow-card__name">${this.esc(f.name || f.label || f.id)}</div>
|
||||
${value ? `<div class="flows-flow-card__value">${value}</div>` : ""}
|
||||
<div class="flows-flow-card__meta">
|
||||
${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 `
|
||||
<div class="funds-detail">
|
||||
<div class="flows-detail">
|
||||
<div class="rapp-nav">
|
||||
<a href="${this.esc(backUrl)}" class="rapp-nav__back">← Flows</a>
|
||||
<span class="rapp-nav__title">${this.esc(this.flowName || "Flow Detail")}</span>
|
||||
${this.isDemo ? '<span class="rapp-nav__badge">Demo</span>' : ""}
|
||||
</div>
|
||||
|
||||
<div class="funds-tabs">
|
||||
<button class="funds-tab ${this.tab === "diagram" ? "funds-tab--active" : ""}" data-tab="diagram">Diagram</button>
|
||||
<button class="funds-tab ${this.tab === "river" ? "funds-tab--active" : ""}" data-tab="river">River</button>
|
||||
<button class="funds-tab ${this.tab === "table" ? "funds-tab--active" : ""}" data-tab="table">Table</button>
|
||||
<button class="funds-tab ${this.tab === "transactions" ? "funds-tab--active" : ""}" data-tab="transactions">Transactions</button>
|
||||
<div class="flows-tabs">
|
||||
<button class="flows-tab ${this.tab === "diagram" ? "flows-tab--active" : ""}" data-tab="diagram">Diagram</button>
|
||||
<button class="flows-tab ${this.tab === "river" ? "flows-tab--active" : ""}" data-tab="river">River</button>
|
||||
<button class="flows-tab ${this.tab === "table" ? "flows-tab--active" : ""}" data-tab="table">Table</button>
|
||||
<button class="flows-tab ${this.tab === "transactions" ? "flows-tab--active" : ""}" data-tab="transactions">Transactions</button>
|
||||
</div>
|
||||
|
||||
<div class="funds-tab-content">
|
||||
${this.loading ? '<div class="funds-loading">Loading...</div>' : this.renderTab()}
|
||||
<div class="flows-tab-content">
|
||||
${this.loading ? '<div class="flows-loading">Loading...</div>' : this.renderTab()}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -385,26 +390,26 @@ class FolkFundsApp extends HTMLElement {
|
|||
const sources = this.nodes.filter((n) => n.type === "source");
|
||||
|
||||
return `
|
||||
<div class="funds-table">
|
||||
<div class="flows-table">
|
||||
${sources.length > 0 ? `
|
||||
<div class="funds-section">
|
||||
<h3 class="funds-section__title">Sources</h3>
|
||||
<div class="funds-cards">
|
||||
<div class="flows-section">
|
||||
<h3 class="flows-section__title">Sources</h3>
|
||||
<div class="flows-cards">
|
||||
${sources.map((n) => this.renderSourceCard(n.data as SourceNodeData, n.id)).join("")}
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<div class="funds-section">
|
||||
<h3 class="funds-section__title">Funnels</h3>
|
||||
<div class="funds-cards">
|
||||
<div class="flows-section">
|
||||
<h3 class="flows-section__title">Funnels</h3>
|
||||
<div class="flows-cards">
|
||||
${funnels.map((n) => this.renderFunnelCard(n.data as FunnelNodeData, n.id)).join("")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="funds-section">
|
||||
<h3 class="funds-section__title">Outcomes</h3>
|
||||
<div class="funds-cards">
|
||||
<div class="flows-section">
|
||||
<h3 class="flows-section__title">Outcomes</h3>
|
||||
<div class="flows-cards">
|
||||
${outcomes.map((n) => this.renderOutcomeCard(n.data as OutcomeNodeData, n.id)).join("")}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -414,21 +419,21 @@ class FolkFundsApp extends HTMLElement {
|
|||
private renderSourceCard(data: SourceNodeData, id: string): string {
|
||||
const allocations = data.targetAllocations || [];
|
||||
return `
|
||||
<div class="funds-card">
|
||||
<div class="funds-card__header">
|
||||
<span class="funds-card__icon">💰</span>
|
||||
<span class="funds-card__label">${this.esc(data.label)}</span>
|
||||
<span class="funds-card__type">${data.sourceType}</span>
|
||||
<div class="flows-card">
|
||||
<div class="flows-card__header">
|
||||
<span class="flows-card__icon">💰</span>
|
||||
<span class="flows-card__label">${this.esc(data.label)}</span>
|
||||
<span class="flows-card__type">${data.sourceType}</span>
|
||||
</div>
|
||||
<div class="funds-card__stat">
|
||||
<span class="funds-card__stat-value">$${data.flowRate.toLocaleString()}</span>
|
||||
<span class="funds-card__stat-label">/month</span>
|
||||
<div class="flows-card__stat">
|
||||
<span class="flows-card__stat-value">$${data.flowRate.toLocaleString()}</span>
|
||||
<span class="flows-card__stat-label">/month</span>
|
||||
</div>
|
||||
${allocations.length > 0 ? `
|
||||
<div class="funds-card__allocs">
|
||||
<div class="flows-card__allocs">
|
||||
${allocations.map((a) => `
|
||||
<div class="funds-card__alloc">
|
||||
<span class="funds-card__alloc-dot" style="background:${a.color}"></span>
|
||||
<div class="flows-card__alloc">
|
||||
<span class="flows-card__alloc-dot" style="background:${a.color}"></span>
|
||||
${a.percentage}% → ${this.esc(this.getNodeLabel(a.targetId))}
|
||||
</div>
|
||||
`).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 `
|
||||
<div class="funds-card">
|
||||
<div class="funds-card__header">
|
||||
<span class="funds-card__icon">🏛</span>
|
||||
<span class="funds-card__label">${this.esc(data.label)}</span>
|
||||
<span class="funds-card__status ${statusClass}">${statusLabel}</span>
|
||||
<div class="flows-card">
|
||||
<div class="flows-card__header">
|
||||
<span class="flows-card__icon">🏛</span>
|
||||
<span class="flows-card__label">${this.esc(data.label)}</span>
|
||||
<span class="flows-card__status ${statusClass}">${statusLabel}</span>
|
||||
</div>
|
||||
<div class="funds-card__bar-container">
|
||||
<div class="funds-card__bar" style="width:${fillPct}%"></div>
|
||||
<div class="funds-card__bar-threshold" style="left:${Math.min(100, (threshold / (data.maxCapacity || 1)) * 100)}%"></div>
|
||||
<div class="flows-card__bar-container">
|
||||
<div class="flows-card__bar" style="width:${fillPct}%"></div>
|
||||
<div class="flows-card__bar-threshold" style="left:${Math.min(100, (threshold / (data.maxCapacity || 1)) * 100)}%"></div>
|
||||
</div>
|
||||
<div class="funds-card__stats">
|
||||
<div class="flows-card__stats">
|
||||
<div>
|
||||
<span class="funds-card__stat-value">$${Math.floor(data.currentValue).toLocaleString()}</span>
|
||||
<span class="funds-card__stat-label">/ $${Math.floor(threshold).toLocaleString()}</span>
|
||||
<span class="flows-card__stat-value">$${Math.floor(data.currentValue).toLocaleString()}</span>
|
||||
<span class="flows-card__stat-label">/ $${Math.floor(threshold).toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="funds-card__stat-value">${Math.round(suffPct)}%</span>
|
||||
<span class="funds-card__stat-label">sufficiency</span>
|
||||
<span class="flows-card__stat-value">${Math.round(suffPct)}%</span>
|
||||
<span class="flows-card__stat-label">sufficiency</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="funds-card__thresholds">
|
||||
<div class="flows-card__thresholds">
|
||||
<span>Min: $${Math.floor(data.minThreshold).toLocaleString()}</span>
|
||||
<span>Max: $${Math.floor(data.maxThreshold).toLocaleString()}</span>
|
||||
<span>Cap: $${Math.floor(data.maxCapacity).toLocaleString()}</span>
|
||||
</div>
|
||||
${data.overflowAllocations.length > 0 ? `
|
||||
<div class="funds-card__allocs">
|
||||
<div class="funds-card__alloc-title">Overflow</div>
|
||||
<div class="flows-card__allocs">
|
||||
<div class="flows-card__alloc-title">Overflow</div>
|
||||
${data.overflowAllocations.map((a) => `
|
||||
<div class="funds-card__alloc">
|
||||
<span class="funds-card__alloc-dot" style="background:${a.color}"></span>
|
||||
<div class="flows-card__alloc">
|
||||
<span class="flows-card__alloc-dot" style="background:${a.color}"></span>
|
||||
${a.percentage}% → ${this.esc(this.getNodeLabel(a.targetId))}
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
` : ""}
|
||||
${data.spendingAllocations.length > 0 ? `
|
||||
<div class="funds-card__allocs">
|
||||
<div class="funds-card__alloc-title">Spending</div>
|
||||
<div class="flows-card__allocs">
|
||||
<div class="flows-card__alloc-title">Spending</div>
|
||||
${data.spendingAllocations.map((a) => `
|
||||
<div class="funds-card__alloc">
|
||||
<span class="funds-card__alloc-dot" style="background:${a.color}"></span>
|
||||
<div class="flows-card__alloc">
|
||||
<span class="flows-card__alloc-dot" style="background:${a.color}"></span>
|
||||
${a.percentage}% → ${this.esc(this.getNodeLabel(a.targetId))}
|
||||
</div>
|
||||
`).join("")}
|
||||
|
|
@ -514,24 +519,24 @@ class FolkFundsApp extends HTMLElement {
|
|||
: "#64748b";
|
||||
|
||||
return `
|
||||
<div class="funds-card">
|
||||
<div class="funds-card__header">
|
||||
<span class="funds-card__icon">🎯</span>
|
||||
<span class="funds-card__label">${this.esc(data.label)}</span>
|
||||
<span class="funds-card__status" style="color:${statusColor}">${data.status}</span>
|
||||
<div class="flows-card">
|
||||
<div class="flows-card__header">
|
||||
<span class="flows-card__icon">🎯</span>
|
||||
<span class="flows-card__label">${this.esc(data.label)}</span>
|
||||
<span class="flows-card__status" style="color:${statusColor}">${data.status}</span>
|
||||
</div>
|
||||
${data.description ? `<div class="funds-card__desc">${this.esc(data.description)}</div>` : ""}
|
||||
<div class="funds-card__bar-container">
|
||||
<div class="funds-card__bar funds-card__bar--outcome" style="width:${fillPct}%;background:${statusColor}"></div>
|
||||
${data.description ? `<div class="flows-card__desc">${this.esc(data.description)}</div>` : ""}
|
||||
<div class="flows-card__bar-container">
|
||||
<div class="flows-card__bar flows-card__bar--outcome" style="width:${fillPct}%;background:${statusColor}"></div>
|
||||
</div>
|
||||
<div class="funds-card__stats">
|
||||
<div class="flows-card__stats">
|
||||
<div>
|
||||
<span class="funds-card__stat-value">$${Math.floor(data.fundingReceived).toLocaleString()}</span>
|
||||
<span class="funds-card__stat-label">/ $${Math.floor(data.fundingTarget).toLocaleString()}</span>
|
||||
<span class="flows-card__stat-value">$${Math.floor(data.fundingReceived).toLocaleString()}</span>
|
||||
<span class="flows-card__stat-label">/ $${Math.floor(data.fundingTarget).toLocaleString()}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="funds-card__stat-value">${Math.round(fillPct)}%</span>
|
||||
<span class="funds-card__stat-label">funded</span>
|
||||
<span class="flows-card__stat-value">${Math.round(fillPct)}%</span>
|
||||
<span class="flows-card__stat-label">funded</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
@ -547,7 +552,7 @@ class FolkFundsApp extends HTMLElement {
|
|||
|
||||
private renderDiagramTab(): string {
|
||||
if (this.nodes.length === 0) {
|
||||
return '<div class="funds-loading">No nodes to display.</div>';
|
||||
return '<div class="flows-loading">No nodes to display.</div>';
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="funds-canvas-container" id="canvas-container">
|
||||
<div class="funds-canvas-badge" id="canvas-badge">
|
||||
<div class="flows-canvas-container" id="canvas-container">
|
||||
<div class="flows-canvas-badge" id="canvas-badge">
|
||||
<div>
|
||||
<div class="funds-canvas-badge__score" id="badge-score" style="color:${scoreColor}">${scorePct}%</div>
|
||||
<div class="funds-canvas-badge__label">ENOUGH</div>
|
||||
<div class="flows-canvas-badge__score" id="badge-score" style="color:${scoreColor}">${scorePct}%</div>
|
||||
<div class="flows-canvas-badge__label">ENOUGH</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="funds-canvas-toolbar">
|
||||
<button class="funds-canvas-btn funds-canvas-btn--source" data-canvas-action="add-source">+ Source</button>
|
||||
<button class="funds-canvas-btn funds-canvas-btn--funnel" data-canvas-action="add-funnel">+ Funnel</button>
|
||||
<button class="funds-canvas-btn funds-canvas-btn--outcome" data-canvas-action="add-outcome">+ Outcome</button>
|
||||
<div class="funds-canvas-sep"></div>
|
||||
<button class="funds-canvas-btn" data-canvas-action="sim" id="sim-btn">${this.isSimulating ? "Pause" : "Play"}</button>
|
||||
<button class="funds-canvas-btn" data-canvas-action="fit">Fit</button>
|
||||
<button class="funds-canvas-btn" data-canvas-action="share">Share</button>
|
||||
<div class="flows-canvas-toolbar">
|
||||
<button class="flows-canvas-btn flows-canvas-btn--source" data-canvas-action="add-source">+ Source</button>
|
||||
<button class="flows-canvas-btn flows-canvas-btn--funnel" data-canvas-action="add-funnel">+ Funnel</button>
|
||||
<button class="flows-canvas-btn flows-canvas-btn--outcome" data-canvas-action="add-outcome">+ Outcome</button>
|
||||
<div class="flows-canvas-sep"></div>
|
||||
<button class="flows-canvas-btn" data-canvas-action="sim" id="sim-btn">${this.isSimulating ? "Pause" : "Play"}</button>
|
||||
<button class="flows-canvas-btn" data-canvas-action="fit">Fit</button>
|
||||
<button class="flows-canvas-btn" data-canvas-action="share">Share</button>
|
||||
</div>
|
||||
<svg class="funds-canvas-svg" id="flow-canvas">
|
||||
<svg class="flows-canvas-svg" id="flow-canvas">
|
||||
<g id="canvas-transform">
|
||||
<g id="edge-layer"></g>
|
||||
<g id="wire-layer"></g>
|
||||
<g id="node-layer"></g>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="funds-editor-panel" id="editor-panel"></div>
|
||||
<div class="funds-canvas-legend">
|
||||
<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#10b981"></span>Source</span>
|
||||
<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#0ea5e9"></span>Funnel</span>
|
||||
<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#f59e0b"></span>Overflow</span>
|
||||
<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#8b5cf6"></span>Spending</span>
|
||||
<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#3b82f6"></span>Outcome</span>
|
||||
<span class="funds-canvas-legend-item"><span class="funds-canvas-legend-dot" style="background:#fbbf24"></span>Sufficient</span>
|
||||
<div class="flows-editor-panel" id="editor-panel"></div>
|
||||
<div class="flows-canvas-legend">
|
||||
<span class="flows-canvas-legend-item"><span class="flows-canvas-legend-dot" style="background:#10b981"></span>Source</span>
|
||||
<span class="flows-canvas-legend-item"><span class="flows-canvas-legend-dot" style="background:#0ea5e9"></span>Funnel</span>
|
||||
<span class="flows-canvas-legend-item"><span class="flows-canvas-legend-dot" style="background:#f59e0b"></span>Overflow</span>
|
||||
<span class="flows-canvas-legend-item"><span class="flows-canvas-legend-dot" style="background:#8b5cf6"></span>Spending</span>
|
||||
<span class="flows-canvas-legend-item"><span class="flows-canvas-legend-dot" style="background:#3b82f6"></span>Outcome</span>
|
||||
<span class="flows-canvas-legend-item"><span class="flows-canvas-legend-dot" style="background:#fbbf24"></span>Sufficient</span>
|
||||
</div>
|
||||
<div class="funds-canvas-zoom">
|
||||
<button class="funds-canvas-btn" data-canvas-action="zoom-in">+</button>
|
||||
<button class="funds-canvas-btn" data-canvas-action="zoom-out">−</button>
|
||||
<div class="flows-canvas-zoom">
|
||||
<button class="flows-canvas-btn" data-canvas-action="zoom-in">+</button>
|
||||
<button class="flows-canvas-btn" data-canvas-action="zoom-out">−</button>
|
||||
</div>
|
||||
<div class="funds-node-tooltip" id="node-tooltip" style="display:none"></div>
|
||||
<div class="flows-node-tooltip" id="node-tooltip" style="display:none"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -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 = `<div class="funds-node-tooltip__label">${this.esc((node.data as any).label)}</div>`;
|
||||
let html = `<div class="flows-node-tooltip__label">${this.esc((node.data as any).label)}</div>`;
|
||||
if (node.type === "source") {
|
||||
const d = node.data as SourceNodeData;
|
||||
html += `<div class="funds-node-tooltip__stat">$${d.flowRate.toLocaleString()}/mo · ${d.sourceType}</div>`;
|
||||
html += `<div class="flows-node-tooltip__stat">$${d.flowRate.toLocaleString()}/mo · ${d.sourceType}</div>`;
|
||||
} else if (node.type === "funnel") {
|
||||
const d = node.data as FunnelNodeData;
|
||||
const suf = computeSufficiencyState(d);
|
||||
html += `<div class="funds-node-tooltip__stat">$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(d.sufficientThreshold ?? d.maxThreshold).toLocaleString()}</div>`;
|
||||
html += `<div class="funds-node-tooltip__stat" style="text-transform:capitalize;color:${suf === "sufficient" || suf === "abundant" ? "#fbbf24" : "#94a3b8"}">${suf}</div>`;
|
||||
html += `<div class="flows-node-tooltip__stat">$${Math.floor(d.currentValue).toLocaleString()} / $${Math.floor(d.sufficientThreshold ?? d.maxThreshold).toLocaleString()}</div>`;
|
||||
html += `<div class="flows-node-tooltip__stat" style="text-transform:capitalize;color:${suf === "sufficient" || suf === "abundant" ? "#fbbf24" : "#94a3b8"}">${suf}</div>`;
|
||||
} else {
|
||||
const d = node.data as OutcomeNodeData;
|
||||
const pct = d.fundingTarget > 0 ? Math.round((d.fundingReceived / d.fundingTarget) * 100) : 0;
|
||||
html += `<div class="funds-node-tooltip__stat">$${Math.floor(d.fundingReceived).toLocaleString()} / $${Math.floor(d.fundingTarget).toLocaleString()} (${pct}%)</div>`;
|
||||
html += `<div class="funds-node-tooltip__stat" style="text-transform:capitalize">${d.status}</div>`;
|
||||
html += `<div class="flows-node-tooltip__stat">$${Math.floor(d.fundingReceived).toLocaleString()} / $${Math.floor(d.fundingTarget).toLocaleString()} (${pct}%)</div>`;
|
||||
html += `<div class="flows-node-tooltip__stat" style="text-transform:capitalize">${d.status}</div>`;
|
||||
}
|
||||
|
||||
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 {
|
|||
<span>${Math.min(phasePct, 100)}% funded</span>
|
||||
<span>$${Math.floor(Math.min(d.fundingReceived, p.fundingThreshold)).toLocaleString()} / $${p.fundingThreshold.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="funds-modal__progress-bar">
|
||||
<div class="funds-modal__progress-fill" style="width:${Math.min(phasePct, 100)}%;background:${unlocked ? "#10b981" : "#3b82f6"}"></div>
|
||||
<div class="flows-modal__progress-bar">
|
||||
<div class="flows-modal__progress-fill" style="width:${Math.min(phasePct, 100)}%;background:${unlocked ? "#10b981" : "#3b82f6"}"></div>
|
||||
</div>
|
||||
</div>
|
||||
${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 = `<div class="funds-modal">
|
||||
<div class="funds-modal__header">
|
||||
backdrop.className = "flows-modal-backdrop";
|
||||
backdrop.id = "flows-modal";
|
||||
backdrop.innerHTML = `<div class="flows-modal">
|
||||
<div class="flows-modal__header">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="display:inline-block;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600;background:${statusColor}22;color:${statusColor}">${statusLabel}</span>
|
||||
<span style="font-size:16px;font-weight:700;color:#e2e8f0">${this.esc(d.label)}</span>
|
||||
</div>
|
||||
<button class="funds-modal__close" data-modal-action="close">×</button>
|
||||
<button class="flows-modal__close" data-modal-action="close">×</button>
|
||||
</div>
|
||||
${d.description ? `<div style="font-size:13px;color:#94a3b8;line-height:1.6;margin-bottom:16px;padding:10px 12px;background:#0f172a;border-radius:8px;border-left:3px solid ${statusColor}">${this.esc(d.description)}</div>` : ""}
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:28px;font-weight:700;color:#e2e8f0">$${Math.floor(d.fundingReceived).toLocaleString()}</div>
|
||||
<div style="font-size:13px;color:#64748b;margin-top:2px">of $${Math.floor(d.fundingTarget).toLocaleString()} (${Math.round(fillPct)}%)</div>
|
||||
<div class="funds-modal__progress-bar" style="margin-top:10px;height:10px">
|
||||
<div class="funds-modal__progress-fill" style="width:${fillPct}%;background:${statusColor}"></div>
|
||||
<div class="flows-modal__progress-bar" style="margin-top:10px;height:10px">
|
||||
<div class="flows-modal__progress-fill" style="width:${fillPct}%;background:${statusColor}"></div>
|
||||
</div>
|
||||
</div>
|
||||
${d.phases && d.phases.length > 0 ? `<div style="margin-bottom:16px">
|
||||
|
|
@ -2018,15 +2089,15 @@ class FolkFundsApp extends HTMLElement {
|
|||
}
|
||||
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.className = "funds-modal-backdrop";
|
||||
backdrop.id = "funds-modal";
|
||||
backdrop.innerHTML = `<div class="funds-modal">
|
||||
<div class="funds-modal__header">
|
||||
backdrop.className = "flows-modal-backdrop";
|
||||
backdrop.id = "flows-modal";
|
||||
backdrop.innerHTML = `<div class="flows-modal">
|
||||
<div class="flows-modal__header">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:20px">${icons[d.sourceType] || "💰"}</span>
|
||||
<span style="font-size:16px;font-weight:700;color:#e2e8f0">${this.esc(d.label)}</span>
|
||||
</div>
|
||||
<button class="funds-modal__close" data-modal-action="close">×</button>
|
||||
<button class="flows-modal__close" data-modal-action="close">×</button>
|
||||
</div>
|
||||
<div style="margin-bottom:16px">
|
||||
<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px">Source Type</div>
|
||||
|
|
@ -2258,7 +2329,7 @@ class FolkFundsApp extends HTMLElement {
|
|||
// ─── River tab ────────────────────────────────────────
|
||||
|
||||
private renderRiverTab(): string {
|
||||
return `<div class="funds-river-container" id="river-mount"></div>`;
|
||||
return `<div class="flows-river-container" id="river-mount"></div>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="funds-tx-empty">
|
||||
<div class="flows-tx-empty">
|
||||
<p>Transaction history is not available in demo mode.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (!this.txLoaded) {
|
||||
return '<div class="funds-loading">Loading transactions...</div>';
|
||||
return '<div class="flows-loading">Loading transactions...</div>';
|
||||
}
|
||||
|
||||
if (this.transactions.length === 0) {
|
||||
return `
|
||||
<div class="funds-tx-empty">
|
||||
<div class="flows-tx-empty">
|
||||
<p>No transactions yet for this flow.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="funds-tx-list">
|
||||
<div class="flows-tx-list">
|
||||
${this.transactions.map((tx) => `
|
||||
<div class="funds-tx">
|
||||
<div class="funds-tx__icon">${tx.type === "deposit" ? "⬆" : tx.type === "withdraw" ? "⬇" : "🔄"}</div>
|
||||
<div class="funds-tx__body">
|
||||
<div class="funds-tx__desc">${this.esc(tx.description || tx.type)}</div>
|
||||
<div class="funds-tx__meta">
|
||||
<div class="flows-tx">
|
||||
<div class="flows-tx__icon">${tx.type === "deposit" ? "⬆" : tx.type === "withdraw" ? "⬇" : "🔄"}</div>
|
||||
<div class="flows-tx__body">
|
||||
<div class="flows-tx__desc">${this.esc(tx.description || tx.type)}</div>
|
||||
<div class="flows-tx__meta">
|
||||
${tx.from ? `From: ${this.esc(tx.from)}` : ""}
|
||||
${tx.to ? ` → ${this.esc(tx.to)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="funds-tx__amount ${tx.type === "deposit" ? "funds-tx__amount--positive" : "funds-tx__amount--negative"}">
|
||||
<div class="flows-tx__amount ${tx.type === "deposit" ? "flows-tx__amount--positive" : "flows-tx__amount--negative"}">
|
||||
${tx.type === "deposit" ? "+" : "-"}$${Math.abs(tx.amount).toLocaleString()}
|
||||
</div>
|
||||
<div class="funds-tx__time">${this.formatTime(tx.timestamp)}</div>
|
||||
<div class="flows-tx__time">${this.formatTime(tx.timestamp)}</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>`;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
<!-- Hero -->
|
||||
<div class="rl-hero">
|
||||
<span class="rl-tagline">rFunds</span>
|
||||
<span class="rl-tagline">rFlows</span>
|
||||
<h1 class="rl-heading" style="background:linear-gradient(to right,#fcd34d,#6ee7b7,#93c5fd);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
|
||||
Threshold-Based Flow Funding
|
||||
</h1>
|
||||
|
|
@ -17,7 +17,7 @@ export function renderLanding(): string {
|
|||
Connect funnels, set overflow rules, and track outcomes in real-time.
|
||||
</p>
|
||||
<div class="rl-cta-row">
|
||||
<a href="https://demo.rspace.online/rfunds" class="rl-cta-secondary" id="ml-primary">Try the Demo</a>
|
||||
<a href="https://demo.rspace.online/rflows" class="rl-cta-secondary" id="ml-primary">Try the Demo</a>
|
||||
<a href="/create-space" class="rl-cta-primary">Create Your Own</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -120,7 +120,7 @@ export function renderLanding(): string {
|
|||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Ecosystem Integration</h2>
|
||||
<p class="rl-subtext" style="text-align:center">
|
||||
rFunds connects to other rSpace modules for end-to-end treasury governance.
|
||||
rFlows connects to other rSpace modules for end-to-end treasury governance.
|
||||
</p>
|
||||
<div class="rl-grid-2" style="max-width:700px;margin:0 auto">
|
||||
<div class="rl-integration">
|
||||
|
|
@ -145,7 +145,7 @@ export function renderLanding(): string {
|
|||
<section class="rl-section rl-section--alt">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Built on Open Source</h2>
|
||||
<p class="rl-subtext" style="text-align:center">The libraries and tools that power rFunds.</p>
|
||||
<p class="rl-subtext" style="text-align:center">The libraries and tools that power rFlows.</p>
|
||||
<div class="rl-grid-3">
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🌊</div>
|
||||
|
|
@ -170,7 +170,7 @@ export function renderLanding(): string {
|
|||
<section class="rl-section">
|
||||
<div class="rl-container" style="text-align:center">
|
||||
<h2 class="rl-heading">Your Data, Protected</h2>
|
||||
<p class="rl-subtext">How rFunds keeps your information safe.</p>
|
||||
<p class="rl-subtext">How rFlows keeps your information safe.</p>
|
||||
<div class="rl-grid-3">
|
||||
<div class="rl-card rl-card--center">
|
||||
<div class="rl-icon-box">🔒</div>
|
||||
|
|
@ -199,7 +199,7 @@ export function renderLanding(): string {
|
|||
<h2 class="rl-heading">Ready to design your funding flows?</h2>
|
||||
<p class="rl-subtext">Start with the interactive demo or create your own space with custom funnels and connections.</p>
|
||||
<div class="rl-cta-row">
|
||||
<a href="https://demo.rspace.online/rfunds" class="rl-cta-primary">Try the Demo</a>
|
||||
<a href="https://demo.rspace.online/rflows" class="rl-cta-primary">Try the Demo</a>
|
||||
<a href="/create-space" class="rl-cta-secondary">Create Your Own</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<FundsDoc>(docId, fundsSchema, binary);
|
||||
this.#documents.open<FlowsDoc>(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<FundsDoc | null> {
|
||||
const docId = fundsDocId(this.#space) as DocumentId;
|
||||
let doc = this.#documents.get<FundsDoc>(docId);
|
||||
async subscribe(): Promise<FlowsDoc | null> {
|
||||
const docId = flowsDocId(this.#space) as DocumentId;
|
||||
let doc = this.#documents.get<FlowsDoc>(docId);
|
||||
if (!doc) {
|
||||
const binary = await this.#store.load(docId);
|
||||
doc = binary
|
||||
? this.#documents.open<FundsDoc>(docId, fundsSchema, binary)
|
||||
: this.#documents.open<FundsDoc>(docId, fundsSchema);
|
||||
? this.#documents.open<FlowsDoc>(docId, flowsSchema, binary)
|
||||
: this.#documents.open<FlowsDoc>(docId, flowsSchema);
|
||||
}
|
||||
await this.#sync.subscribe([docId]);
|
||||
return doc ?? null;
|
||||
}
|
||||
|
||||
getFlows(): FundsDoc | undefined {
|
||||
return this.#documents.get<FundsDoc>(fundsDocId(this.#space) as DocumentId);
|
||||
getFlows(): FlowsDoc | undefined {
|
||||
return this.#documents.get<FlowsDoc>(flowsDocId(this.#space) as DocumentId);
|
||||
}
|
||||
|
||||
addSpaceFlow(flow: SpaceFlow): void {
|
||||
const docId = fundsDocId(this.#space) as DocumentId;
|
||||
this.#sync.change<FundsDoc>(docId, `Add flow ${flow.flowId}`, (d) => {
|
||||
const docId = flowsDocId(this.#space) as DocumentId;
|
||||
this.#sync.change<FlowsDoc>(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<FundsDoc>(docId, `Remove flow ${flowId}`, (d) => {
|
||||
const docId = flowsDocId(this.#space) as DocumentId;
|
||||
this.#sync.change<FlowsDoc>(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); }
|
||||
|
|
|
|||
|
|
@ -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<FundsDoc>(docId);
|
||||
function ensureDoc(space: string): FlowsDoc {
|
||||
const docId = flowsDocId(space);
|
||||
let doc = _syncServer!.getDoc<FlowsDoc>(docId);
|
||||
if (!doc) {
|
||||
doc = Automerge.change(Automerge.init<FundsDoc>(), 'init', (d) => {
|
||||
const init = fundsSchema.init();
|
||||
doc = Automerge.change(Automerge.init<FlowsDoc>(), '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<FundsDoc>(docId, 'add space flow', (d) => {
|
||||
_syncServer!.changeDoc<FlowsDoc>(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<FundsDoc>(docId);
|
||||
const docId = flowsDocId(space);
|
||||
const doc = _syncServer!.getDoc<FlowsDoc>(docId);
|
||||
if (doc) {
|
||||
const key = `${space}:${flowId}`;
|
||||
if (doc.spaceFlows[key]) {
|
||||
_syncServer!.changeDoc<FundsDoc>(docId, 'remove space flow', (d) => {
|
||||
_syncServer!.changeDoc<FlowsDoc>(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 = `
|
||||
<script src="https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js"></script>
|
||||
<script type="module" src="/modules/rfunds/folk-funds-app.js"></script>
|
||||
<script type="module" src="/modules/rfunds/folk-budget-river.js"></script>`;
|
||||
<script type="module" src="/modules/rflows/folk-flows-app.js"></script>
|
||||
<script type="module" src="/modules/rflows/folk-flow-river.js"></script>`;
|
||||
|
||||
const fundsStyles = `<link rel="stylesheet" href="/modules/rfunds/funds.css">`;
|
||||
const flowsStyles = `<link rel="stylesheet" href="/modules/rflows/flows.css">`;
|
||||
|
||||
// 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: `<folk-funds-app space="${spaceSlug}"${spaceSlug === "demo" ? ' mode="demo"' : ''}></folk-funds-app>`,
|
||||
scripts: fundsScripts,
|
||||
styles: fundsStyles,
|
||||
body: `<folk-flows-app space="${spaceSlug}"${spaceSlug === "demo" ? ' mode="demo"' : ''}></folk-flows-app>`,
|
||||
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: `<folk-funds-app space="${spaceSlug}" flow-id="${flowId}"></folk-funds-app>`,
|
||||
scripts: fundsScripts,
|
||||
styles: flowsStyles,
|
||||
body: `<folk-flows-app space="${spaceSlug}" flow-id="${flowId}"></folk-flows-app>`,
|
||||
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<FundsDoc>(docId, 'seed template flow', (d) => {
|
||||
_syncServer.changeDoc<FlowsDoc>(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",
|
||||
|
|
|
|||
|
|
@ -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<FundsDoc> = {
|
||||
module: 'funds',
|
||||
collection: 'flows',
|
||||
export const flowsSchema: DocSchema<FlowsDoc> = {
|
||||
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<FundsDoc> = {
|
|||
|
||||
// ── Helpers ──
|
||||
|
||||
export function fundsDocId(space: string) {
|
||||
return `${space}:funds:flows` as const;
|
||||
export function flowsDocId(space: string) {
|
||||
return `${space}:flows:data` as const;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ class FolkNotesApp extends HTMLElement {
|
|||
Accommodation: EUR 1200 (30%)
|
||||
Activities: EUR 1000 (25%)
|
||||
Food: EUR 600 (15%)
|
||||
Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rFunds. Current spend: EUR 1,203.</em></p>`,
|
||||
Gear: EUR 400 (10%)</code></pre><p><em>Maya is tracking expenses in rFlows. Current spend: EUR 1,203.</em></p>`,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export function renderLanding(): string {
|
|||
<div class="rl-integration">
|
||||
<div class="rl-icon-box">📈</div>
|
||||
<div>
|
||||
<h3><a href="/rfunds" style="color:#14b8a6;text-decoration:none">rFunds</a></h3>
|
||||
<h3><a href="/rflows" style="color:#14b8a6;text-decoration:none">rFlows</a></h3>
|
||||
<p>Overlay budget categories on your wallet data to see where funds are allocated and how they flow.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ const DEMO_SHAPES: Record<string, unknown>[] = [
|
|||
store: "StarRent.eu",
|
||||
},
|
||||
|
||||
// ─── rFunds: Expenses ───────────────────────────────────────
|
||||
// ─── rFlows: Expenses ───────────────────────────────────────
|
||||
{
|
||||
id: "demo-expense-shuttle",
|
||||
type: "demo-expense",
|
||||
|
|
@ -307,7 +307,7 @@ const DEMO_SHAPES: Record<string, unknown>[] = [
|
|||
date: "2026-07-13",
|
||||
},
|
||||
|
||||
// ─── rFunds: Budget ─────────────────────────────────────────
|
||||
// ─── rFlows: Budget ─────────────────────────────────────────
|
||||
{
|
||||
id: "demo-budget-trip",
|
||||
type: "folk-budget",
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ const TEMPLATE_SHAPES: Record<string, unknown>[] = [
|
|||
store: "",
|
||||
},
|
||||
|
||||
// ─── rFunds: Budget ─────────────────────────────────────────
|
||||
// ─── rFlows: Budget ─────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-budget",
|
||||
type: "folk-budget",
|
||||
|
|
@ -129,7 +129,7 @@ const TEMPLATE_SHAPES: Record<string, unknown>[] = [
|
|||
],
|
||||
},
|
||||
|
||||
// ─── rFunds: Expense ────────────────────────────────────────
|
||||
// ─── rFlows: Expense ────────────────────────────────────────
|
||||
{
|
||||
id: "tmpl-expense",
|
||||
type: "demo-expense",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
|||
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<string, string> = {
|
|||
rforum: "Communicating",
|
||||
rchoices: "Deciding",
|
||||
rvote: "Deciding",
|
||||
rfunds: "Funding & Commerce",
|
||||
rflows: "Funding & Commerce",
|
||||
rwallet: "Funding & Commerce",
|
||||
rcart: "Funding & Commerce",
|
||||
rauctions: "Funding & Commerce",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
|||
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<string, string> = {
|
|||
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",
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -1803,7 +1803,7 @@
|
|||
<button id="new-spider-3d" title="3D Spider Plot">📊 3D Spider</button>
|
||||
<button id="new-conviction" title="Conviction Ranking">⏳ Conviction</button>
|
||||
<button id="new-token" title="New Token">🪙 Token</button>
|
||||
<button id="embed-funds" title="Embed rFunds">🌊 rFunds</button>
|
||||
<button id="embed-flows" title="Embed rFlows">🌊 rFlows</button>
|
||||
<button id="embed-wallet" title="Embed rWallet">💰 rWallet</button>
|
||||
<button id="embed-vote" title="Embed rVote">🗳️ rVote</button>
|
||||
</div>
|
||||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
<span class="hl">EncryptID session</span>. 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.
|
||||
</p>
|
||||
|
|
@ -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 @@
|
|||
<div class="rl-card" style="text-align: left;">
|
||||
<h3>Your data, connected across tools</h3>
|
||||
<p>
|
||||
A budget created in <span class="hl">rFunds</span> can reference a vote
|
||||
A budget created in <span class="hl">rFlows</span> can reference a vote
|
||||
from <span class="hl">rVote</span>. A map pin in <span class="hl">rMaps</span>
|
||||
can link to files in <span class="hl">rFiles</span> and notes in
|
||||
<span class="hl">rNotes</span>. Because all r-Ecosystem tools share the same
|
||||
|
|
@ -473,7 +473,7 @@
|
|||
<a href="https://rspace.online/rfiles" class="ecosystem-app">📁 rFiles</a>
|
||||
<a href="https://rspace.online/rnotes" class="ecosystem-app">📝 rNotes</a>
|
||||
<a href="https://rspace.online/rtrips" class="ecosystem-app">✈ rTrips</a>
|
||||
<a href="https://rspace.online/rfunds" class="ecosystem-app">💸 rFunds</a>
|
||||
<a href="https://rspace.online/rflows" class="ecosystem-app">💸 rFlows</a>
|
||||
<a href="https://rspace.online/rnetwork" class="ecosystem-app">🕸️ rNetwork</a>
|
||||
<a href="https://rspace.online/rcart" class="ecosystem-app">🛒 rCart</a>
|
||||
<a href="https://rspace.online/rtube" class="ecosystem-app">🎬 rTube</a>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue