feat(deck): add spend-breakdown slide and refine balances

Add a "Where the money went" slide after the cash-over-time chart, using a
bar chart of expenses by category (salaries, materials, operations,
contributed equity, fuel) sourced from the ledger — it answers the obvious
"where did the burn go?" question the cash curve leaves open.

Rework the balances slide into two labelled zones (Liabilities the
collective owes vs Equity claims on it) with a divider and larger type,
rather than a proportional chart that would render a $65 balance invisible
next to $100k.

Also fix BarChart value labels: an absent Vue Boolean prop casts to false,
so the old `showValues !== false` guard always hid the amounts — inverted to
`hideValues`. Rename Waterfall -> WaterfallChart for the multi-word lint rule
and enlarge chart type for projector readability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-07-30 20:54:30 +02:00
commit a6f6b5bb15
11 changed files with 139 additions and 69 deletions

View file

@ -2,24 +2,26 @@
import { computed } from 'vue'
import { palette, money } from '../data'
// Note: Vue casts an absent Boolean prop to `false`, so a "showValues" default
// of true isn't possible without a runtime default invert it to hideValues.
const props = defineProps<{
items: readonly { label: string; amount: number; color: string }[]
showValues?: boolean
hideValues?: boolean
}>()
const max = computed(() => Math.max(...props.items.map((i) => i.amount), 1))
</script>
<template>
<div class="space-y-4">
<div class="space-y-5">
<div
v-for="(item, i) in items"
:key="item.label"
class="grid grid-cols-[9rem_1fr] items-center gap-4"
class="grid grid-cols-[10rem_1fr] items-center gap-5"
>
<span class="truncate text-right text-sm text-slate-300">{{ item.label }}</span>
<div class="flex items-center gap-3">
<div class="h-7 flex-1 overflow-hidden rounded-md bg-slate-800/60">
<span class="truncate text-right text-base text-slate-300 sm:text-lg">{{ item.label }}</span>
<div class="flex items-center gap-4">
<div class="h-9 flex-1 overflow-hidden rounded-md bg-slate-800/60">
<div
class="h-full rounded-md animate-in slide-in-from-left duration-700 fill-mode-both"
:class="palette[item.color]?.bar ?? 'bg-slate-500'"
@ -27,8 +29,8 @@ const max = computed(() => Math.max(...props.items.map((i) => i.amount), 1))
/>
</div>
<span
v-if="showValues !== false"
class="w-20 shrink-0 text-right text-sm font-semibold tabular-nums text-slate-200"
v-if="!hideValues"
class="w-28 shrink-0 text-right text-lg font-bold tabular-nums text-slate-100 sm:text-xl"
>
{{ money(item.amount) }}
</span>

View file

@ -0,0 +1,37 @@
<script setup lang="ts">
import SlideShell from '../components/SlideShell.vue'
import BarChart from '../components/BarChart.vue'
import { expenses, money } from '../data'
</script>
<template>
<SlideShell kicker="Step 2 · The books over time" title="Where the money went">
<div class="grid gap-12 lg:grid-cols-[1.25fr_1fr] lg:items-center">
<div>
<p class="mb-8 text-lg text-slate-400">
Total spent to date: <span class="font-semibold text-slate-100">{{ money(60615) }}</span>
</p>
<BarChart :items="expenses" />
</div>
<div class="space-y-6">
<p class="text-xl leading-relaxed text-slate-300 sm:text-2xl">
The curve falling isn't the whole story
<span class="font-semibold text-slate-100">what it bought</span>
is.
</p>
<p class="text-lg leading-relaxed text-slate-300">
<span class="font-semibold text-sky-400">Salaries</span> are the biggest line this is a
team building something. <span class="font-semibold text-amber-400">Materials</span> come
next: the prototypes.
</p>
<div class="rounded-2xl border border-emerald-500/30 bg-emerald-500/10 p-5">
<p class="text-base leading-relaxed text-emerald-100 sm:text-lg">
Even the <span class="font-semibold">{{ money(65) }}</span> of fuel a member fronted is
on the books. Nothing is too small to see that's the point.
</p>
</div>
</div>
</div>
</SlideShell>
</template>

View file

@ -1,53 +0,0 @@
<script setup lang="ts">
import SlideShell from '../components/SlideShell.vue'
import { balances, money } from '../data'
</script>
<template>
<SlideShell kicker="Because it's a collective" title="Everyone holds a balance with the entity">
<p class="max-w-3xl text-lg leading-relaxed text-slate-300">
In a collective, people aren't just staff — they lend, spend, and own. A person's balance
takes one of two shapes, and the books tell them apart automatically.
</p>
<div class="mt-8 grid gap-5 sm:grid-cols-3">
<div
v-for="(b, i) in balances"
:key="b.name"
class="rounded-2xl border p-6 animate-in fade-in slide-in-from-bottom-4 fill-mode-both duration-500"
:class="
b.kind === 'liability'
? 'border-rose-500/40 bg-rose-500/10'
: 'border-sky-500/40 bg-sky-500/10'
"
:style="{ animationDelay: 150 + i * 120 + 'ms' }"
>
<div class="flex items-center justify-between">
<span class="text-lg font-bold text-slate-100">{{ b.name }}</span>
<span
class="rounded-full px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wide"
:class="
b.kind === 'liability' ? 'bg-rose-500/20 text-rose-300' : 'bg-sky-500/20 text-sky-300'
"
>
{{ b.kind }}
</span>
</div>
<p class="mt-4 text-3xl font-bold tabular-nums text-slate-50">{{ money(b.amount) }}</p>
<p class="mt-3 text-sm text-slate-300">{{ b.story }}</p>
<p class="mt-3 border-t border-slate-100/10 pt-3 text-sm text-slate-400">{{ b.meaning }}</p>
</div>
</div>
<div class="mt-8 flex flex-wrap gap-x-10 gap-y-2 text-sm">
<p class="text-slate-300">
<span class="font-semibold text-rose-400">Liability</span> = the collective owes you. A debt
to be repaid.
</p>
<p class="text-slate-300">
<span class="font-semibold text-sky-400">Equity</span> = you own a slice. A claim on future
profit.
</p>
</div>
</SlideShell>
</template>

View file

@ -0,0 +1,82 @@
<script setup lang="ts">
import { ArrowDownLeft, ArrowUpRight } from '@lucide/vue'
import SlideShell from '../components/SlideShell.vue'
import { balances, money } from '../data'
const owed = balances.filter((b) => b.kind === 'liability')
const owned = balances.filter((b) => b.kind === 'equity')
</script>
<template>
<SlideShell kicker="Because it's a collective" title="Everyone holds a balance with the entity">
<p class="max-w-4xl text-xl leading-relaxed text-slate-300 sm:text-2xl">
In a collective, people aren't just staff they lend, spend, and own. Every balance is one of
two shapes, and the books tell them apart automatically.
</p>
<div
class="mt-10 grid gap-8 lg:grid-cols-[minmax(0,1fr)_auto_minmax(0,1.4fr)] lg:items-stretch"
>
<!-- LIABILITIES: what the collective owes -->
<div>
<div class="mb-4 flex items-center gap-3">
<div class="grid size-11 place-items-center rounded-xl bg-rose-500/15 text-rose-400">
<ArrowDownLeft class="size-6" />
</div>
<div>
<p class="text-xl font-bold text-rose-400 sm:text-2xl">Owed by the collective</p>
<p class="text-sm uppercase tracking-widest text-slate-500">
Liabilities · a debt to repay
</p>
</div>
</div>
<div
v-for="b in owed"
:key="b.name"
class="rounded-2xl border border-rose-500/40 bg-rose-500/10 p-6"
>
<div class="flex items-baseline justify-between">
<span class="text-2xl font-bold text-slate-50">{{ b.name }}</span>
<span class="text-3xl font-bold tabular-nums text-rose-300">{{ money(b.amount) }}</span>
</div>
<p class="mt-3 text-base text-slate-300">{{ b.story }}</p>
</div>
</div>
<!-- divider -->
<div
class="hidden w-px bg-gradient-to-b from-transparent via-slate-600 to-transparent lg:block"
/>
<!-- EQUITY: claims on the collective -->
<div>
<div class="mb-4 flex items-center gap-3">
<div class="grid size-11 place-items-center rounded-xl bg-sky-500/15 text-sky-400">
<ArrowUpRight class="size-6" />
</div>
<div>
<p class="text-xl font-bold text-sky-400 sm:text-2xl">Owned of the collective</p>
<p class="text-sm uppercase tracking-widest text-slate-500">
Equity · a claim on the upside
</p>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div
v-for="b in owned"
:key="b.name"
class="rounded-2xl border border-sky-500/40 bg-sky-500/10 p-6"
>
<div class="flex items-baseline justify-between gap-2">
<span class="text-2xl font-bold text-slate-50">{{ b.name }}</span>
<span class="text-2xl font-bold tabular-nums text-sky-300 sm:text-3xl">{{
money(b.amount)
}}</span>
</div>
<p class="mt-3 text-base text-slate-300">{{ b.story }}</p>
</div>
</div>
</div>
</div>
</SlideShell>
</template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import SlideShell from '../components/SlideShell.vue'
import Waterfall, { type WaterfallBar } from '../components/Waterfall.vue'
import WaterfallChart, { type WaterfallBar } from '../components/WaterfallChart.vue'
import { money } from '../data'
const MINUS = ''
@ -17,7 +17,7 @@ const bars: WaterfallBar[] = [
<template>
<SlideShell kicker="As of 30 Jul 2026" title="How every dollar adds up today">
<Waterfall :bars="bars" :y-max="116000" />
<WaterfallChart :bars="bars" :y-max="116000" />
<p class="mt-8 text-center text-lg text-slate-300 sm:text-xl">
Raised <span class="font-semibold text-sky-300">{{ money(103000) }}</span

View file

@ -6,12 +6,13 @@ import PrinciplesSlide from './03-Principles.vue'
import ProjectSlide from './04-Project.vue'
import InvestmentSlide from './05-Investment.vue'
import LedgerSlide from './06-Ledger.vue'
import ServersSlide from './07-Servers.vue'
import BalancesSlide from './08-Balances.vue'
import SnapshotSlide from './09-Snapshot.vue'
import AppSlide from './10-App.vue'
import ValueSlide from './11-Value.vue'
import FavaSlide from './12-Fava.vue'
import SpendSlide from './07-Spend.vue'
import ServersSlide from './08-Servers.vue'
import BalancesSlide from './09-Balances.vue'
import SnapshotSlide from './10-Snapshot.vue'
import AppSlide from './11-App.vue'
import ValueSlide from './12-Value.vue'
import FavaSlide from './13-Fava.vue'
export interface Slide {
title: string
@ -25,6 +26,7 @@ export const slides: Slide[] = [
{ title: 'Meet Project Lantern', component: ProjectSlide },
{ title: 'The money arrives', component: InvestmentSlide },
{ title: 'The books over time', component: LedgerSlide },
{ title: 'Where the money went', component: SpendSlide },
{ title: 'The $5,000 servers', component: ServersSlide },
{ title: 'Everyone has a balance', component: BalancesSlide },
{ title: 'Where we stand today', component: SnapshotSlide },