Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified paper/figures/formulation_a_optima.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions paper/slides/presentation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# dependencies
/node_modules

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# typescript
*.tsbuildinfo
next-env.d.ts

# generated deck PDF
/slides.pdf
39 changes: 39 additions & 0 deletions paper/slides/presentation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# VAT microsimulation presentation

Conference slide app for the IMA World Congress 2026 talk on
*A Firm-Level Microsimulation for VAT Policy Analysis*.

Built on the PolicyEngine slideshow template (Next.js 16 / React 19 / Tailwind v4,
KaTeX for math). Slides are React components; a client-side viewer swaps between
them via keyboard (`→`/`←`/`Space`, `Home`/`End`, `f` for fullscreen) or click.

## Commands

Install dependencies from this directory:

```bash
npm install
```

Run the local deck:

```bash
npm run dev
```

Check the app:

```bash
npm run typecheck
npm run lint
npm run build
```

Export the deck to PDF (screenshots each slide via Playwright):

```bash
npm run export:pdf # one-time: npm i -D playwright pdf-lib && npx playwright install chromium
```

The deck is defined in `slides/config.ts` (order + metadata) and
`slides/vat-ima-2026.tsx` (the slides). Figures live in `public/figures/`.
112 changes: 112 additions & 0 deletions paper/slides/presentation/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap");

@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
@import "katex/dist/katex.min.css";

@source "../app";
@source "../components";
@source "../lib";
@source "../slides";

@theme {
--font-display: var(--font-sans);
--spacing: 0.25rem;

--text-5xl: 3rem;
--text-5xl--line-height: 1;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--text-7xl: 4.5rem;
--text-7xl--line-height: 1;

--color-pe-teal: #319795;
--color-pe-dark: #234e52;
--color-pe-darker: #0d2f33;
--color-pe-light: #e6fffa;
--color-pe-amber: #e8913a;
}

:root {
--slide-bg: #ffffff;
--slide-ink: #101828;
--slide-muted: #64748b;
--pe-teal: #319795;
--pe-teal-dark: #2c7a7b;
--pe-dark: #234e52;
--pe-darker: #0d2f33;
--pe-light: #e6fffa;
--pe-amber: #e8913a;
--border-light: #e2e8f0;
}

* {
box-sizing: border-box;
}

html,
body {
margin: 0;
min-height: 100%;
background: var(--slide-bg);
color: var(--slide-ink);
font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

button {
font: inherit;
}

@utility gradient-bg {
background:
linear-gradient(135deg, var(--pe-teal) 0%, var(--pe-teal-dark) 38%, var(--pe-dark) 72%, var(--pe-darker) 100%);
overflow: hidden;
position: relative;
}

.gradient-bg::before {
background:
radial-gradient(ellipse 80% 50% at 18% 118%, rgba(255, 255, 255, 0.12) 0%, transparent 50%),
radial-gradient(ellipse 60% 40% at 96% 10%, rgba(232, 145, 58, 0.10) 0%, transparent 42%);
content: "";
inset: 0;
pointer-events: none;
position: absolute;
}

@utility gradient-footer {
background: var(--pe-dark);
position: relative;
}

.gradient-footer::before {
background: linear-gradient(90deg, var(--pe-teal) 0%, var(--pe-amber) 100%);
content: "";
height: 2px;
left: 0;
position: absolute;
right: 0;
top: 0;
}

.slide-active {
opacity: 1;
transition: opacity 150ms ease-in-out;
}

.slide-exit {
opacity: 0;
}

.math-text {
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

.figure-grid {
background-image:
linear-gradient(rgba(49, 151, 149, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(49, 151, 149, 0.08) 1px, transparent 1px);
background-size: 32px 32px;
}
20 changes: 20 additions & 0 deletions paper/slides/presentation/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "A Firm-Level Microsimulation for VAT Policy Analysis",
description:
"PolicyEngine conference deck for the IMA World Congress 2026 — costing UK VAT registration-threshold reforms on synthetic firm data.",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
8 changes: 8 additions & 0 deletions paper/slides/presentation/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use client";

import SlideshowViewer from "@/components/core/SlideshowViewer";
import { vatIma2026Config } from "@/slides/config";

export default function Home() {
return <SlideshowViewer config={vatIma2026Config} />;
}
56 changes: 56 additions & 0 deletions paper/slides/presentation/components/content/AutoFitMath.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { ReactNode, useLayoutEffect, useRef, useState } from "react";

/**
* Scales its child down (never up) so wide content fits the available width.
* Used for KaTeX equations on slides, where horizontal scrolling is not an
* option during a live talk. Measurement uses offsetWidth, which is the
* untransformed layout width, so applying the transform never feeds back.
*/
export default function AutoFitMath({
children,
className = "",
}: {
children: ReactNode;
className?: string;
}) {
const outerRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);

useLayoutEffect(() => {
const outer = outerRef.current;
const inner = innerRef.current;
if (!outer || !inner) return;

const fit = () => {
const available = outer.clientWidth;
const natural = inner.offsetWidth;
if (natural > 0 && available > 0) {
setScale(natural > available ? available / natural : 1);
}
};

fit();
const ro = new ResizeObserver(fit);
ro.observe(outer);
// KaTeX fonts can change the measured width once they load.
if (typeof document !== "undefined" && document.fonts?.ready) {
document.fonts.ready.then(fit).catch(() => {});
}
return () => ro.disconnect();
}, [children]);

return (
<div ref={outerRef} className={`w-full overflow-hidden text-center ${className}`}>
<div
ref={innerRef}
className="inline-block"
style={{ transform: `scale(${scale})`, transformOrigin: "center" }}
>
{children}
</div>
</div>
);
}
20 changes: 20 additions & 0 deletions paper/slides/presentation/components/content/BulletList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ReactNode } from "react";

interface BulletListProps {
/** Each item may be plain text or rich JSX (e.g. inline <Math />). */
items: ReactNode[];
className?: string;
}

export default function BulletList({ items, className = "" }: BulletListProps) {
return (
<ul className={`space-y-5 ${className}`}>
{items.map((item, index) => (
<li key={index} className="flex gap-4 text-xl leading-snug text-slate-700">
<span className="mt-2.5 h-2.5 w-2.5 flex-none rounded-full bg-pe-teal" />
<span>{item}</span>
</li>
))}
</ul>
);
}
31 changes: 31 additions & 0 deletions paper/slides/presentation/components/content/ContentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ReactNode } from "react";

interface ContentCardProps {
title?: string;
children: ReactNode;
accent?: "teal" | "amber" | "slate";
className?: string;
}

export default function ContentCard({
title,
children,
accent = "teal",
className = "",
}: ContentCardProps) {
const accentClass =
accent === "amber"
? "border-t-pe-amber"
: accent === "slate"
? "border-t-slate-400"
: "border-t-pe-teal";

return (
<div
className={`rounded-lg border border-slate-200 border-t-4 ${accentClass} bg-white p-7 shadow-sm ${className}`}
>
{title && <h2 className="mb-4 text-2xl font-bold text-pe-dark">{title}</h2>}
{children}
</div>
);
}
23 changes: 23 additions & 0 deletions paper/slides/presentation/components/content/EquationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import AutoFitMath from "@/components/content/AutoFitMath";
import Math from "@/components/content/Math";

interface EquationCardProps {
title: string;
/** LaTeX source rendered as display math via KaTeX, auto-fit to the card width. */
equation: string;
note: string;
}

export default function EquationCard({ title, equation, note }: EquationCardProps) {
return (
<div className="rounded-lg border border-slate-200 bg-white p-8 shadow-sm">
<div className="text-2xl font-bold text-pe-dark">{title}</div>
<div className="mt-8 rounded-md bg-slate-50 px-6 py-6 text-slate-800">
<AutoFitMath>
<Math tex={equation} display className="text-[1.3rem]" />
</AutoFitMath>
</div>
<p className="mt-6 text-xl leading-snug text-slate-600">{note}</p>
</div>
);
}
23 changes: 23 additions & 0 deletions paper/slides/presentation/components/content/Figure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Image from "@/components/core/BasePathImage";

interface FigureProps {
src: string;
alt: string;
width: number;
height: number;
}

export default function Figure({ src, alt, width, height }: FigureProps) {
return (
<div className="flex h-full w-full items-center justify-center">
<Image
alt={alt}
src={src}
width={width}
height={height}
className="h-auto max-h-full w-full object-contain"
style={{ height: "auto" }}
/>
</div>
);
}
Loading
Loading