@@ -295,7 +298,7 @@ ${renderFilterStateCss(viewModel)}
margin-bottom: 16px;
font-family: 'Berkeley Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: 18px; }
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); align-items: start; gap: 18px; }
.card {
display: block;
text-decoration: none;
@@ -311,11 +314,13 @@ ${renderFilterStateCss(viewModel)}
display: block;
width: 100%;
aspect-ratio: 16 / 9;
- object-fit: cover;
+ object-fit: contain;
background: #000;
outline: 1px solid rgba(255,255,255,0.08);
outline-offset: -1px;
}
+ .card[data-orientation="portrait"] .preview { aspect-ratio: 3 / 4; }
+ .card[data-orientation="square"] .preview { aspect-ratio: 1; }
.card-body { display: flex; align-items: flex-start; gap: 10px; padding: 14px; }
.item-info { flex: 1; min-width: 0; }
.item-name { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -522,6 +527,14 @@ function renderPutioTitle(title: string): string {
return escapeHtml(title).replace("put.io", "put.io");
}
+function screenshotOrientation(screenshot: VrefScreenshot): ScreenshotOrientation {
+ if (screenshot.viewport.width === screenshot.viewport.height) {
+ return "square";
+ }
+
+ return screenshot.viewport.width > screenshot.viewport.height ? "landscape" : "portrait";
+}
+
function filterInputId(group: string, value: string): string {
return `filter-${group}-${value}`;
}
diff --git a/test/vref.test.ts b/test/vref.test.ts
index 6fb2216..57e3c38 100644
--- a/test/vref.test.ts
+++ b/test/vref.test.ts
@@ -31,6 +31,8 @@ describe("vref", () => {
expect(html).toContain("padding: 10px 0 16px");
expect(html).toContain("min-height: 24px");
expect(html).toContain("minmax(min(100%, 340px), 1fr)");
+ expect(html).toContain('data-orientation="landscape"');
+ expect(html).toContain("object-fit: contain");
expect(html).toContain("1 reference · Updated May 19, 2026");
expect(html).toContain(".footer code { color: var(--text-2); font: inherit; }");
expect(html).toContain(
@@ -52,6 +54,67 @@ describe("vref", () => {
expect(html).not.toContain(">TV<");
});
+ it("derives portrait and square card layouts from viewport dimensions", async () => {
+ const root = await mkdtemp(join(tmpdir(), "vref-"));
+ await mkdir(join(root, ".vref/screenshots"), { recursive: true });
+ await writeFile(join(root, ".vref/screenshots/phone.png"), "image");
+ await writeFile(join(root, ".vref/screenshots/square.png"), "image");
+
+ const manifest: VrefManifest = {
+ version: 1,
+ title: "put.io mobile visual reference",
+ description: "Mobile screenshots.",
+ updatedAt: "2026-05-19T13:35:00.000Z",
+ screenshots: [
+ {
+ id: "phone",
+ title: "Phone",
+ group: "Screens",
+ platform: "iOS",
+ device: "iPhone",
+ viewport: { width: 1320, height: 2868 },
+ file: "screenshots/phone.png",
+ capturedAt: "2026-05-19T13:34:00.000Z",
+ sizeBytes: 5,
+ tags: ["phone"],
+ notes: ["Portrait screen."],
+ },
+ {
+ id: "square",
+ title: "Square",
+ group: "Components",
+ platform: "iOS",
+ device: "Component",
+ viewport: { width: 720, height: 720 },
+ file: "screenshots/square.png",
+ capturedAt: "2026-05-19T13:34:00.000Z",
+ sizeBytes: 5,
+ tags: ["component"],
+ notes: ["Square component."],
+ },
+ ],
+ };
+
+ await writeFile(join(root, ".vref/manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
+
+ await buildGallery({
+ cwd: root,
+ manifestPath: ".vref/manifest.json",
+ outputPath: ".vref/index.html",
+ });
+
+ const html = await readFile(join(root, ".vref/index.html"), "utf8");
+ expect(html).toContain(
+ 'data-title="Phone" data-platform="ios" data-group="screens" data-tags="phone" data-orientation="portrait"',
+ );
+ expect(html).toContain(
+ 'data-title="Square" data-platform="ios" data-group="components" data-tags="component" data-orientation="square"',
+ );
+ expect(html).toContain('.card[data-orientation="portrait"] .preview { aspect-ratio: 3 / 4; }');
+ expect(html).toContain('.card[data-orientation="square"] .preview { aspect-ratio: 1; }');
+ expect(html).toContain("align-items: start");
+ });
+
it("keeps single tags filterable without rendering a card tag chip", async () => {
const root = await makeFixture("screenshots/roku-720p/home.jpg", ["home"]);