diff --git a/index.html b/index.html index edd4762..cd41067 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Boilerplate Website + Accounting for the Collective — Project Lantern
diff --git a/src/features/deck/DeckView.vue b/src/features/deck/DeckView.vue new file mode 100644 index 0000000..71419f5 --- /dev/null +++ b/src/features/deck/DeckView.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/features/deck/components/AreaChart.vue b/src/features/deck/components/AreaChart.vue new file mode 100644 index 0000000..3b6e840 --- /dev/null +++ b/src/features/deck/components/AreaChart.vue @@ -0,0 +1,110 @@ + + + diff --git a/src/features/deck/components/BarChart.vue b/src/features/deck/components/BarChart.vue new file mode 100644 index 0000000..e3c2fdf --- /dev/null +++ b/src/features/deck/components/BarChart.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/features/deck/components/SlideShell.vue b/src/features/deck/components/SlideShell.vue new file mode 100644 index 0000000..7cadf42 --- /dev/null +++ b/src/features/deck/components/SlideShell.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/features/deck/components/StackedBar.vue b/src/features/deck/components/StackedBar.vue new file mode 100644 index 0000000..e96f366 --- /dev/null +++ b/src/features/deck/components/StackedBar.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/features/deck/components/StatCard.vue b/src/features/deck/components/StatCard.vue new file mode 100644 index 0000000..9f92663 --- /dev/null +++ b/src/features/deck/components/StatCard.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/features/deck/components/Waterfall.vue b/src/features/deck/components/Waterfall.vue new file mode 100644 index 0000000..f952abc --- /dev/null +++ b/src/features/deck/components/Waterfall.vue @@ -0,0 +1,99 @@ + + + diff --git a/src/features/deck/data.ts b/src/features/deck/data.ts new file mode 100644 index 0000000..5d51af2 --- /dev/null +++ b/src/features/deck/data.ts @@ -0,0 +1,129 @@ +// --------------------------------------------------------------------------- +// Canonical figures for Project Lantern. +// These mirror ledger/lantern.beancount exactly so the deck and Fava agree. +// (Verified against `bean-check` + Fava's own totals on the ledger — see README.) +// --------------------------------------------------------------------------- + +export const money = (n: number) => + n.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }) + +export const money2 = (n: number) => + n.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }) + +// The plan the seed round was raised against (what the deck calls "the budget"). +export const SEED = 100_000 + +export const budget = [ + { label: 'Salaries', amount: 50_000, color: 'sky' }, + { label: 'Hardware & Materials', amount: 30_000, color: 'amber' }, + { label: 'Operations', amount: 12_000, color: 'violet' }, + { label: 'Contingency', amount: 8_000, color: 'slate' }, +] as const + +// Cash on hand at the end of each month (Assets:Bank + Assets:Cash), from the +// ledger. Used for the "books over time" area chart. +export const cashByMonth = [ + { month: 'Jan', cash: 92_850 }, + { month: 'Feb', cash: 85_700 }, + { month: 'Mar', cash: 70_550 }, // card paid off + $5k servers capitalized out of cash + { month: 'Apr', cash: 58_835 }, + { month: 'May', cash: 45_685 }, + { month: 'Jun', cash: 42_535 }, // first $4k revenue lands, burn nearly flat + { month: 'Jul', cash: 43_885 }, // revenue > spend — the curve turns up +] as const + +// Expenses to date, by category (cash + the one non-cash equity grant). +export const expenses = [ + { label: 'Salaries', amount: 36_000, color: 'sky' }, + { label: 'Materials', amount: 13_500, color: 'amber' }, + { label: 'Operations', amount: 8_050, color: 'violet' }, + { label: 'Contributed (equity)', amount: 3_000, color: 'emerald' }, + { label: 'Fuel', amount: 65, color: 'rose' }, +] as const + +// Balance-sheet snapshot as of 2026-07-30, straight from the ledger. +export const snapshot = { + assets: { + total: 48_885, + rows: [ + { label: 'Bank — Checking', amount: 42_885 }, + { label: 'Cash — Petty cash float', amount: 1_000 }, + { label: 'Equipment — Servers', amount: 5_000 }, + ], + }, + liabilities: { + total: 0, + rows: [{ label: 'Outstanding reimbursements', amount: 0 }], + }, + equity: { + total: 48_885, + rows: [ + { label: 'Investor capital (seed)', amount: 100_000 }, + { label: 'Contributor equity (Bob)', amount: 3_000 }, + { label: 'Retained earnings (to date)', amount: -54_115 }, + ], + }, + revenueToDate: 6_500, + expensesToDate: 60_615, +} + +// People with a balance against the collective — the two shapes it can take. +export const balances = [ + { + name: 'Alice', + kind: 'liability' as const, + amount: 65, + story: 'Fronted her own cash for fuel in the org vehicle.', + meaning: 'The collective OWES Alice. A debt, cleared when she is paid back.', + }, + { + name: 'Bob', + kind: 'equity' as const, + amount: 3_000, + story: 'Contributed engineering work, took equity instead of cash.', + meaning: 'Bob OWNS a slice — a claim on future profit, not a debt to repay.', + }, + { + name: 'Atitlán Ventures', + kind: 'equity' as const, + amount: 100_000, + story: 'Wrote the seed cheque that started the project.', + meaning: 'The investor holds the largest claim on the upside.', + }, +] + +export const team = [ + { name: 'Pat', role: 'Software' }, + { name: 'Rayan', role: 'Accountant' }, +] + +// Tailwind class fragments keyed by our palette names (kept explicit so the +// JIT compiler always sees the full class string). +export const palette: Record = { + sky: { bar: 'bg-sky-500', text: 'text-sky-400', dot: 'bg-sky-500', soft: 'bg-sky-500/15' }, + amber: { + bar: 'bg-amber-500', + text: 'text-amber-400', + dot: 'bg-amber-500', + soft: 'bg-amber-500/15', + }, + violet: { + bar: 'bg-violet-500', + text: 'text-violet-400', + dot: 'bg-violet-500', + soft: 'bg-violet-500/15', + }, + emerald: { + bar: 'bg-emerald-500', + text: 'text-emerald-400', + dot: 'bg-emerald-500', + soft: 'bg-emerald-500/15', + }, + rose: { bar: 'bg-rose-500', text: 'text-rose-400', dot: 'bg-rose-500', soft: 'bg-rose-500/15' }, + slate: { + bar: 'bg-slate-500', + text: 'text-slate-400', + dot: 'bg-slate-500', + soft: 'bg-slate-500/15', + }, +} diff --git a/src/features/deck/slides/01-Title.vue b/src/features/deck/slides/01-Title.vue new file mode 100644 index 0000000..0f3a0e7 --- /dev/null +++ b/src/features/deck/slides/01-Title.vue @@ -0,0 +1,41 @@ + + + diff --git a/src/features/deck/slides/02-Problem.vue b/src/features/deck/slides/02-Problem.vue new file mode 100644 index 0000000..2034f18 --- /dev/null +++ b/src/features/deck/slides/02-Problem.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/features/deck/slides/03-Principles.vue b/src/features/deck/slides/03-Principles.vue new file mode 100644 index 0000000..01dac2d --- /dev/null +++ b/src/features/deck/slides/03-Principles.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/features/deck/slides/04-Project.vue b/src/features/deck/slides/04-Project.vue new file mode 100644 index 0000000..245aebc --- /dev/null +++ b/src/features/deck/slides/04-Project.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/features/deck/slides/05-Investment.vue b/src/features/deck/slides/05-Investment.vue new file mode 100644 index 0000000..8c03f1a --- /dev/null +++ b/src/features/deck/slides/05-Investment.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/features/deck/slides/06-Ledger.vue b/src/features/deck/slides/06-Ledger.vue new file mode 100644 index 0000000..310c78e --- /dev/null +++ b/src/features/deck/slides/06-Ledger.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/features/deck/slides/07-Servers.vue b/src/features/deck/slides/07-Servers.vue new file mode 100644 index 0000000..82a6cfd --- /dev/null +++ b/src/features/deck/slides/07-Servers.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/features/deck/slides/08-Balances.vue b/src/features/deck/slides/08-Balances.vue new file mode 100644 index 0000000..336d7d7 --- /dev/null +++ b/src/features/deck/slides/08-Balances.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/features/deck/slides/09-Snapshot.vue b/src/features/deck/slides/09-Snapshot.vue new file mode 100644 index 0000000..83ced83 --- /dev/null +++ b/src/features/deck/slides/09-Snapshot.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/features/deck/slides/10-App.vue b/src/features/deck/slides/10-App.vue new file mode 100644 index 0000000..87a3ae5 --- /dev/null +++ b/src/features/deck/slides/10-App.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/features/deck/slides/11-Value.vue b/src/features/deck/slides/11-Value.vue new file mode 100644 index 0000000..75298d6 --- /dev/null +++ b/src/features/deck/slides/11-Value.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/features/deck/slides/12-Fava.vue b/src/features/deck/slides/12-Fava.vue new file mode 100644 index 0000000..1e313df --- /dev/null +++ b/src/features/deck/slides/12-Fava.vue @@ -0,0 +1,68 @@ + + + diff --git a/src/features/deck/slides/index.ts b/src/features/deck/slides/index.ts new file mode 100644 index 0000000..def6aab --- /dev/null +++ b/src/features/deck/slides/index.ts @@ -0,0 +1,34 @@ +import type { Component } from 'vue' + +import TitleSlide from './01-Title.vue' +import ProblemSlide from './02-Problem.vue' +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' + +export interface Slide { + title: string + component: Component +} + +export const slides: Slide[] = [ + { title: 'Accounting for the Collective', component: TitleSlide }, + { title: 'The problem', component: ProblemSlide }, + { title: 'Accounting in 90 seconds', component: PrinciplesSlide }, + { title: 'Meet Project Lantern', component: ProjectSlide }, + { title: 'The money arrives', component: InvestmentSlide }, + { title: 'The books over time', component: LedgerSlide }, + { title: 'The $5,000 servers', component: ServersSlide }, + { title: 'Everyone has a balance', component: BalancesSlide }, + { title: 'Where we stand today', component: SnapshotSlide }, + { title: 'The application', component: AppSlide }, + { title: 'Why it matters', component: ValueSlide }, + { title: 'This is real, not a mockup', component: FavaSlide }, +] diff --git a/src/features/deck/useDeck.ts b/src/features/deck/useDeck.ts new file mode 100644 index 0000000..3f04345 --- /dev/null +++ b/src/features/deck/useDeck.ts @@ -0,0 +1,64 @@ +import { onMounted, onUnmounted, ref } from 'vue' + +// Small, self-contained deck controller: current slide index, next/prev, and +// keyboard + URL-hash sync so any slide is directly linkable (#3). +export function useDeck(total: number) { + const index = ref(clampFromHash(total)) + + function clamp(n: number) { + return Math.max(0, Math.min(total - 1, n)) + } + + function go(n: number) { + index.value = clamp(n) + window.location.hash = String(index.value + 1) + } + + const next = () => go(index.value + 1) + const prev = () => go(index.value - 1) + + function onKey(e: KeyboardEvent) { + switch (e.key) { + case 'ArrowRight': + case 'PageDown': + case ' ': + e.preventDefault() + next() + break + case 'ArrowLeft': + case 'PageUp': + e.preventDefault() + prev() + break + case 'Home': + e.preventDefault() + go(0) + break + case 'End': + e.preventDefault() + go(total - 1) + break + } + } + + function onHashChange() { + index.value = clampFromHash(total) + } + + onMounted(() => { + window.addEventListener('keydown', onKey) + window.addEventListener('hashchange', onHashChange) + }) + onUnmounted(() => { + window.removeEventListener('keydown', onKey) + window.removeEventListener('hashchange', onHashChange) + }) + + return { index, next, prev, go } +} + +function clampFromHash(total: number) { + const n = parseInt(window.location.hash.replace('#', ''), 10) + if (Number.isNaN(n)) return 0 + return Math.max(0, Math.min(total - 1, n - 1)) +} diff --git a/src/router/index.ts b/src/router/index.ts index 4f315f4..ad0397e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -3,8 +3,8 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' const routes: RouteRecordRaw[] = [ { path: '/', - name: 'home', - component: () => import('@/views/HomeView.vue'), + name: 'deck', + component: () => import('@/features/deck/DeckView.vue'), }, ] diff --git a/src/style.css b/src/style.css index 3ebb69e..4c32d12 100644 --- a/src/style.css +++ b/src/style.css @@ -80,4 +80,10 @@ body { @apply bg-background text-foreground; } + /* Scale the whole deck with the viewport. Every Tailwind rem-based size + (text, padding, gaps, max-width) is relative to this root font-size, so + bumping it enlarges the presentation uniformly to fill large screens. */ + html { + font-size: clamp(16px, 0.85vw + 6px, 23px); + } }