Skip to content
Merged
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
6 changes: 6 additions & 0 deletions assets/css/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ code {
}

@layer base {
/* Prevent rubber-band bounce from exposing the area behind the fixed frame. */
html,
body {
overscroll-behavior-y: none;
}

[id] { scroll-margin-top: 5rem; }

@media (prefers-reduced-motion: reduce) {
Expand Down
2 changes: 1 addition & 1 deletion layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="site-frame bg-gradient-to-b from-orange-600 to-red-600 p-3">
<main
class="site-noise-surface noise-bg min-h-[calc(100dvh-24px)] bg-white dark:bg-black"
class="site-noise-surface noise-bg min-h-[calc(100dvh-24px)] bg-white [--noise-opacity:0.08] dark:bg-black dark:[--noise-opacity:0.28]"
>
<Header />
<slot />
Expand Down
15 changes: 15 additions & 0 deletions pages/talks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ useHead({
});

const talks = [
{
title: "Data Outpost 2026",
subtitle: "Building an analytics agent with eve",
date: new Date("2026-11-04"),
href_slides: "/slides/data-outpost-2026.pdf",
},
{
title: "NY Stripe Developer Meetup",
subtitle:
"The eve Framework: Agentic payments and building a shopping assistant",
date: new Date("2026-10-22"),
href_slides: "/slides/stripe-developer-meetup-ny-2026.pdf",
},
{
title: "All Things Open 2025",
subtitle: "Enabling community education with a little help from AI",
Expand Down Expand Up @@ -139,6 +152,8 @@ const formatDate = (date) => {
v-if="talk.href_slides"
class="text-xs text-gray-400 uppercase hover:cursor-pointer hover:text-orange-500"
:href="talk.href_slides"
target="_blank"
rel="noopener noreferrer"
>
[slide_deck]
</a>
Expand Down
Binary file added public/slides/data-outpost-2026.pdf
Binary file not shown.
Binary file added public/slides/stripe-developer-meetup-ny-2026.pdf
Binary file not shown.
45 changes: 32 additions & 13 deletions scripts/generate-responsive-images.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
import { mkdir, rm } from "node:fs/promises";
import { mkdir, rename, rm } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { resolve, basename, extname } from "node:path";
import sharp from "sharp";
import { experimentLinks } from "../data/experiments.mjs";

const output = resolve("public/images/generated");
await rm(output, { recursive: true, force: true });
await mkdir(output, { recursive: true });

// Keep assets available to a running dev server while regenerating. Write each
// replacement beside its destination, then rename it atomically into place.
async function publishImage(image, filename) {
const temporary = resolve(output, `.${randomUUID()}-${filename}`);
try {
const info = await image.toFile(temporary);
await rename(temporary, resolve(output, filename));
return info;
} finally {
await rm(temporary, { force: true });
}
}

// Publish the shared texture before processing the larger thumbnail set.
// The UI renders it in grayscale; 16 gray levels preserve the treatment.
await publishImage(
sharp("public/images/noise.png")
.grayscale()
.png({ palette: true, colours: 16, effort: 10 }),
"noise.png",
);

const images = [
...new Set(experimentLinks.map(({ img }) => img).filter(Boolean)),
];
Expand All @@ -14,24 +37,20 @@ for (const src of images) {
for (const width of [320, 640, 960]) {
let size = Infinity;
for (const quality of [68, 58, 48, 38, 28, 20]) {
const info = await sharp(resolve("public", src.slice(1)))
.rotate()
.resize(width, width, { fit: "cover" })
.webp({ quality, effort: 5 })
.toFile(resolve(output, `${name}-${width}.webp`));
const info = await publishImage(
sharp(resolve("public", src.slice(1)))
.rotate()
.resize(width, width, { fit: "cover" })
.webp({ quality, effort: 5 }),
`${name}-${width}.webp`,
);
size = info.size;
if (size <= 100_000) break;
}
if (size > 100_000)
throw new Error(`Thumbnail budget exceeded: ${name}-${width}`);
}
}
// The UI already renders the texture in grayscale. Quantizing to 16 gray levels
// preserves the noise treatment while avoiding a 49 kB RGB texture per visit.
await sharp("public/images/noise.png")
.grayscale()
.png({ palette: true, colours: 16, effort: 10 })
.toFile(resolve(output, "noise.png"));
console.log(
`Generated ${images.length * 3} responsive thumbnails and noise texture`,
);
101 changes: 101 additions & 0 deletions tests/e2e/site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,107 @@ async function navigate(page: Page, path: string) {
}, path);
}

for (const colorScheme of ["light", "dark"] as const) {
test(`page texture stays subtle in ${colorScheme} mode`, async ({ page }) => {
await page.emulateMedia({ colorScheme });
await page.goto("/");
const surface = page.locator(".site-noise-surface");
await expect(surface).toHaveCSS(
"background-color",
colorScheme === "light" ? "rgb(255, 255, 255)" : "rgb(0, 0, 0)",
);
await expect
.poll(() =>
surface.evaluate(
(element) => getComputedStyle(element, "::before").opacity,
),
)
.toBe(colorScheme === "light" ? "0.08" : "0.28");
});

test(`menu icon contrasts with both layouts in ${colorScheme} mode`, async ({
page,
}) => {
await page.emulateMedia({ colorScheme });
for (const route of ["/", "/talks/", "/experiments/midi/"]) {
await page.goto(route);
const menu = page.getByLabel("Toggle navigation");
await expect(menu).toBeVisible();
const contrast = await menu.evaluate((element) => {
const context = document.createElement("canvas").getContext("2d")!;
const luminance = (color: string) => {
context.clearRect(0, 0, 1, 1);
context.fillStyle = color;
context.fillRect(0, 0, 1, 1);
const channels = [...context.getImageData(0, 0, 1, 1).data]
.slice(0, 3)
.map((value) => {
const channel = value / 255;
return channel <= 0.04045
? channel / 12.92
: ((channel + 0.055) / 1.055) ** 2.4;
});
return (
channels[0]! * 0.2126 +
channels[1]! * 0.7152 +
channels[2]! * 0.0722
);
};
let surface: Element | null = element;
let background = "rgb(255, 255, 255)";
while (surface) {
const color = getComputedStyle(surface).backgroundColor;
if (color !== "rgba(0, 0, 0, 0)" && color !== "transparent") {
background = color;
break;
}
surface = surface.parentElement;
}
const foreground = luminance(
getComputedStyle(element.querySelector("svg")!).fill,
);
const backdrop = luminance(background);
return (
(Math.max(foreground, backdrop) + 0.05) /
(Math.min(foreground, backdrop) + 0.05)
);
});
expect(
contrast,
`${route} menu contrast in ${colorScheme} mode`,
).toBeGreaterThanOrEqual(4.5);
await menu.click();
await expect(
page.getByRole("navigation", { name: "Primary navigation" }),
).toBeVisible();
}
});
}

test("document disables vertical overscroll without trapping normal scrolling", async ({
page,
}) => {
for (const route of ["/", "/experiments/midi/"]) {
await page.goto(route);
await expect(page.locator("html")).toHaveCSS(
"overscroll-behavior-y",
"none",
);
await expect(page.locator("body")).toHaveCSS(
"overscroll-behavior-y",
"none",
);
}
await page.goto("/");
await page.mouse.move(180, 400);
await page.mouse.wheel(0, -2000);
await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0);
await page.mouse.wheel(0, 500);
await expect
.poll(() => page.evaluate(() => window.scrollY))
.toBeGreaterThan(0);
});

test("normal navigation resets document scroll; back and anchors restore it", async ({
page,
}) => {
Expand Down
Loading