Rebuild stack-folder with sticky tab rail and site previews.

L0–L7 folders stack on scroll with aligned max depth, labeled tabs that
stay consistent when L7 joins the rail, Cal embeds, preview screenshots,
Playwright tests, and updated README.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-21 21:30:05 -04:00
co-authored by Cursor
parent 09b0f498ba
commit 21c75cdcba
28 changed files with 1669 additions and 348 deletions
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env node
/** Refresh stack-folder/previews/*.png — run dev server first for local shots */
import { chromium } from 'playwright';
import { mkdirSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const out = join(root, 'stack-folder/previews');
const base = process.env.PREVIEW_BASE || 'http://localhost:5177';
mkdirSync(out, { recursive: true });
const shots = [
['spec', `${base}/spec/`],
['stack', `${base}/stack/`],
['auto', 'https://auto.levkin.ca'],
['caseware', 'https://caseware.levkin.ca'],
['iliadobkin', 'https://iliadobkin.com'],
['git-repos', 'https://git.levkin.ca/explore/repos'],
['cal', 'https://cal.levkin.ca/ilia/consult'],
];
async function setCalTheme(page, mode) {
await page.evaluate((want) => {
const root = document.documentElement;
const btn = [...document.querySelectorAll('button, [role="button"], label, a')].find(
(el) => new RegExp(want, 'i').test(el.textContent || el.getAttribute('aria-label') || ''),
);
if (btn) btn.click();
if (want === 'dark') {
root.dataset.theme = 'dark';
root.classList.add('dark');
} else {
root.dataset.theme = 'light';
root.classList.remove('dark');
}
}, mode);
}
async function captureCal(page, outDir, theme) {
const url = 'https://cal.levkin.ca/ilia/consult';
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 25000 });
await page.waitForTimeout(1200);
await setCalTheme(page, theme);
await page.waitForTimeout(1500);
await page.evaluate(() => {
window.scrollTo(0, 120);
});
await page.waitForTimeout(600);
const file = theme === 'light' ? 'cal-light.png' : 'cal-dark.png';
await page.screenshot({
path: join(outDir, file),
clip: { x: 0, y: 0, width: 1280, height: 680 },
});
console.log(`${file}`);
}
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
for (const [name, url] of shots) {
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
await page.waitForTimeout(1500);
await page.screenshot({ path: join(out, `${name}.png`) });
console.log(`${name}.png`);
} catch (err) {
console.warn(`${name}: ${err.message}`);
}
}
try {
await captureCal(page, out, 'dark');
await captureCal(page, out, 'light');
} catch (err) {
console.warn(`✗ cal captures: ${err.message}`);
}
await browser.close();
+64
View File
@@ -0,0 +1,64 @@
/**
* Tab rail alignment tests for /stack-folder/.
* Run: STACK_URL=http://localhost:5173/stack-folder/ npm run test:folder
*/
import { chromium } from 'playwright';
const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/';
const VERBOSE = process.env.VERBOSE === '1';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
await page.goto(URL, { waitUntil: 'networkidle' });
async function tabTops() {
return page.evaluate(() => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const stick =
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--stack-stick')) * 16;
return {
scrollY: window.scrollY,
isFolded: document.querySelector('.mount')?.classList.contains('is-folded'),
stick,
tabs: tabs.map((t) => ({
code: t.querySelector('.tab-code')?.textContent,
top: Math.round(t.getBoundingClientRect().top),
left: Math.round(t.getBoundingClientRect().left),
})),
};
});
}
if (VERBOSE) console.log('initial', await tabTops());
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForTimeout(600);
if (VERBOSE) console.log('max scroll', await tabTops());
await page.click('[data-goto="7"]');
await page.waitForTimeout(800);
if (VERBOSE) console.log('goto L7', await tabTops());
const fail = await page.evaluate(() => {
const tabs = [...document.querySelectorAll('.mount .tab')];
const tops = tabs.map((t) => t.getBoundingClientRect().top);
const min = Math.min(...tops);
const max = Math.max(...tops);
const l7 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L7');
const l0 = tabs.find((t) => t.querySelector('.tab-code')?.textContent === 'L0');
const issues = [];
if (max - min > 30) issues.push(`tab row spread ${Math.round(max - min)}px (want ≤30)`);
if (l7 && l0 && Math.abs(l7.getBoundingClientRect().top - l0.getBoundingClientRect().top) > 30)
issues.push('L7 not aligned with L0');
if (!document.querySelector('.mount')?.classList.contains('is-folded'))
issues.push('mount not folded at max scroll');
return issues;
});
await browser.close();
if (fail.length) {
console.error('FAIL:', fail.join('; '));
process.exit(1);
}
console.log('PASS: stack-folder tab rail');
+118
View File
@@ -0,0 +1,118 @@
/**
* Automated scroll/blur tests for stack-folder.
* Run: node scripts/test-stack-scroll.mjs
*/
import { chromium } from 'playwright';
const URL = process.env.STACK_URL || 'http://localhost:5173/stack-folder/';
const VIEWPORT = { width: 1280, height: 800 };
function fail(msg) {
console.error('FAIL:', msg);
process.exitCode = 1;
}
function pass(msg) {
console.log('PASS:', msg);
}
async function readState(page) {
return page.evaluate(() => {
const stick =
parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
const l7Tab = document.querySelector('.f7 .tab');
const l0 = document.querySelector('.f0');
const l0Body = document.querySelector('.f0 .body');
const folderBlur = l0?.style.getPropertyValue('--stack-blur') || '';
const filter = l0Body ? getComputedStyle(l0Body).filter : '';
const blurMatch = filter.match(/blur\(([\d.]+)px\)/);
const blurPx = blurMatch ? parseFloat(blurMatch[1]) : 0;
return {
scrollY: window.scrollY,
docMax: document.documentElement.scrollHeight - innerHeight,
l7TabTop: l7Tab?.getBoundingClientRect().top ?? null,
stick,
l0Covered: l0?.classList.contains('is-covered'),
l0BlurPx: blurPx,
folderBlur,
l0Filter: filter,
runway: getComputedStyle(document.querySelector('.mount')).getPropertyValue(
'--stack-runway',
),
};
});
}
async function main() {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: VIEWPORT });
await page.goto(URL, { waitUntil: 'networkidle' });
await page.waitForTimeout(400);
const top = await readState(page);
if (top.l0Covered || top.l0BlurPx > 0.1) {
fail(`L0 blurred at top (covered=${top.l0Covered}, blur=${top.l0BlurPx})`);
} else {
pass('L0 clear at scroll top');
}
await page.evaluate(() => window.scrollTo(0, 999999));
await page.waitForTimeout(350);
const end = await readState(page);
if (end.l7TabTop === null) fail('L7 tab missing');
else if (Math.abs(end.l7TabTop - end.stick) > 8) {
fail(`L7 tab not on stick: top=${end.l7TabTop} stick=${end.stick} scrollY=${end.scrollY}`);
} else {
pass(`L7 on stick at max scroll (y=${end.scrollY}, tabTop=${end.l7TabTop.toFixed(1)})`);
}
if (end.l0BlurPx < 2) {
fail(`L0 not blurred when stacked (blur=${end.l0BlurPx})`);
} else {
pass(`L0 blurred when stacked (blur=${end.l0BlurPx}px)`);
}
const midY = Math.floor(end.scrollY * 0.45);
await page.evaluate((y) => window.scrollTo(0, y), midY);
await page.waitForTimeout(200);
const mid = await readState(page);
if (mid.l0BlurPx > 2) {
fail(`L0 still heavily blurred mid-scroll out (blur=${mid.l0BlurPx} at y=${mid.scrollY})`);
} else {
pass(`L0 unfades when scrolling out (blur=${mid.l0BlurPx}px at y=${mid.scrollY})`);
}
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(350);
const back = await readState(page);
if (back.l0Covered || back.l0BlurPx > 0.1) {
fail(`L0 still blurred after scroll to top (covered=${back.l0Covered}, blur=${back.l0BlurPx})`);
} else {
pass('L0 unfades after scroll back to top');
}
const maxY = end.scrollY;
await page.evaluate((y) => window.scrollTo(0, y), maxY);
await page.waitForTimeout(200);
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(350);
const back2 = await readState(page);
if (back2.l0Covered || back2.l0BlurPx > 0.1) {
fail(`L0 stuck after down-up cycle (blur=${back2.l0BlurPx})`);
} else {
pass('L0 unfades after full down-up cycle');
}
await browser.close();
if (process.exitCode) {
console.log('\nTests failed.');
process.exit(1);
}
console.log('\nAll tests passed.');
}
main().catch((e) => {
console.error(e);
process.exit(1);
});