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
Binary file modified public/images/app-dark-1280.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/app-dark-1920.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/app-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/app-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/app-light-1280.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/app-light-1920.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/app-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/app-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/data-grid-dark-1216.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/data-grid-dark-2432.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/data-grid-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/data-grid-light-1216.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/data-grid-light-2432.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/data-grid-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/sql-editor-dark-1216.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/sql-editor-dark-2432.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/sql-editor-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/sql-editor-light-1216.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/sql-editor-light-2432.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/features/sql-editor-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion resources/js/components/landing/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export default function Hero({ githubStars, latestRelease }: Props) {
'/images/app-dark-1280.webp 1280w, /images/app-dark-1920.webp 1920w, /images/app-dark.webp 3024w',
}}
sizes="(max-width: 1280px) 1280px, (max-width: 1920px) 1920px, 3024px"
alt="TablePro on macOS: a SQL query and its result grid, with the connection sidebar on the left."
alt="TablePro on macOS: the orders table open in the data grid, sixty rows deep, with the connection sidebar on the left."
width={3024}
height={1720}
priority
Expand Down
37 changes: 32 additions & 5 deletions resources/js/components/landing/workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ interface Shot {
/** Base name under /images/features. The 1216/2432 WebP ladder is derived from it. */
name: string;
alt: string;
/**
* Which edge of the screenshot carries its subject.
*
* The cell blows every shot up to 145% and clips it, so only 1/1.45 of the
* image — 69% — is ever on the page. Anchoring used to follow row parity,
* which is a fact about the layout and not about the picture: it put the
* AI assistant's panel, which lives on the right of that shot, in the 31%
* that gets cut. The row was captioned "AI Assistant" above a screenshot
* with no assistant in it. Parity still decides which side the cell sits on
* and where its hairline goes; the picture decides this.
*/
anchor: 'left' | 'right';
}

/**
Expand Down Expand Up @@ -60,6 +72,7 @@ const ROWS: Row[] = [
shot: {
name: 'sql-editor',
alt: 'The SQL editor with a multi-statement query and its result tabs below.',
anchor: 'left',
},
rows: [
{
Expand Down Expand Up @@ -91,7 +104,8 @@ const ROWS: Row[] = [
body: 'Every column type gets its own editor: a calendar for timestamps, a searchable list of referenced rows for foreign keys, a three-state checkbox for nullable booleans. Nothing reaches the database until you press Save.',
shot: {
name: 'data-grid',
alt: 'The data grid with edited cells highlighted and a pending-changes count in the toolbar.',
alt: 'The data grid with an edited cell highlighted and the save control live, the change still only in memory.',
anchor: 'right',
},
rows: [
{
Expand All @@ -113,7 +127,8 @@ const ROWS: Row[] = [
body: 'Explain a query, optimize it, or fix one that failed. Suggestions arrive as a before-and-after diff with numbered steps tagged Critical, Change or Context.',
shot: {
name: 'ai-assistant',
alt: 'The AI assistant showing a before and after diff of a rewritten query with numbered explanation steps.',
alt: 'The AI assistant answering a question in a side panel, with the SQL it generated and a step-by-step explanation.',
anchor: 'right',
},
rows: [
{
Expand Down Expand Up @@ -189,9 +204,21 @@ export default function Workbench() {

return (
<div key={row.index}>
{/*
* The column template flips with the row, not just the
* order. `order-2` moves the copy cell past the shot in
* placement order but leaves the track sizes alone, so a
* fixed `[minmax(0,24rem)_1fr]` handed every even row's
* screenshot the 24rem track and its prose the 1fr one.
* Row 02 rendered a 2432px shot into 384px of cell — the
* width `shotSources` sizes the whole ladder against is
* 1216 — and nothing in that window was legible.
*/}
<div
className={`lg:grid lg:grid-cols-[minmax(0,24rem)_1fr] lg:items-stretch ${
isEven ? 'lg:[&>*:first-child]:order-2' : ''
className={`lg:grid lg:items-stretch ${
isEven
? 'lg:grid-cols-[1fr_minmax(0,24rem)] lg:[&>*:first-child]:order-2'
: 'lg:grid-cols-[minmax(0,24rem)_1fr]'
}`}
>
<div className="p-6 sm:p-8 lg:p-10">
Expand Down Expand Up @@ -225,7 +252,7 @@ export default function Workbench() {
width={2432}
height={1385}
sizes="(min-width: 1024px) 1216px, 100vw"
className={`lg:w-[145%] lg:max-w-none ${isEven ? 'lg:-translate-x-[31%]' : ''}`}
className={`lg:w-[145%] lg:max-w-none ${row.shot.anchor === 'right' ? 'lg:-translate-x-[31%]' : ''}`}
/>
</div>
</div>
Expand Down
191 changes: 191 additions & 0 deletions tests/Feature/Landing/ImageAssetsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

/**
* Every screenshot on this site is captured by hand and converted to its WebP
* ladder by hand, and until now nothing checked the result. Three failures are
* cheap to make and invisible in review: a path that no longer resolves, a
* `srcSet` descriptor that claims a width the file does not have — which makes
* the browser pick the wrong candidate and either blur the image or download
* the largest one on a phone — and a dark variant re-shot at a different window
* size, which shifts the layout for half the visitors and nobody else.
*
* These run on the files themselves rather than on rendered markup, so they
* cover the paths that are assembled at runtime too.
*/

/**
* @return list<string>
*/
function frontendSourceFiles(): array
{
$files = [];
$tree = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(resource_path('js'), FilesystemIterator::SKIP_DOTS),
);

foreach ($tree as $file) {
if (in_array($file->getExtension(), ['ts', 'tsx'], true)) {
$files[] = $file->getPathname();
}
}

return $files;
}

/**
* @return list<string>
*/
function publicImageFiles(): array
{
$files = [];
$tree = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(public_path('images'), FilesystemIterator::SKIP_DOTS),
);

foreach ($tree as $file) {
if (in_array(strtolower($file->getExtension()), ['png', 'webp', 'jpg', 'jpeg'], true)) {
$files[] = $file->getPathname();
}
}

sort($files);

return $files;
}

/**
* Literal `/images/...` paths only. A path built from a template literal has no
* extension at the point of reference and is skipped here; the two assertions
* that walk `public/images` cover those files instead.
*
* @return array<string, string> Map of web path to the source file naming it.
*/
function referencedImagePaths(): array
{
$paths = [];

foreach (frontendSourceFiles() as $file) {
preg_match_all('#/images/[A-Za-z0-9@._/-]+\.(?:png|webp|jpe?g|svg|avif)#', (string) file_get_contents($file), $matches);

foreach ($matches[0] as $path) {
$paths[$path] ??= str_replace(base_path() . '/', '', $file);
}
}

ksort($paths);

return $paths;
}

/**
* @return array{0: int, 1: int}
*/
function imageDimensions(string $absolutePath): array
{
$size = @getimagesize($absolutePath);

if ($size === false) {
return [0, 0];
}

return [$size[0], $size[1]];
}

it('serves every image the frontend links to', function (): void {
$missing = [];
$referenced = referencedImagePaths();

expect($referenced)->not->toBeEmpty();

foreach ($referenced as $path => $source) {
if (! is_file(public_path(ltrim($path, '/')))) {
$missing[] = "{$path} referenced by {$source}";
}
}

expect($missing)->toBe([]);
});

it('gives every srcSet candidate the width its descriptor claims', function (): void {
$wrong = [];
$checked = 0;

foreach (frontendSourceFiles() as $file) {
preg_match_all('#(/images/[A-Za-z0-9@._/-]+\.webp)\s+(\d+)w#', (string) file_get_contents($file), $matches, PREG_SET_ORDER);

foreach ($matches as $match) {
[, $path, $declared] = $match;
$absolute = public_path(ltrim($path, '/'));

if (! is_file($absolute)) {
continue;
}

[$actual] = imageDimensions($absolute);
$checked++;

if ($actual !== (int) $declared) {
$wrong[] = "{$path} declares {$declared}w but is {$actual}px wide";
}
}
}

expect($wrong)->toBe([]);
expect($checked)->toBeGreaterThan(0);
});

/**
* The ladder is generated from the base shot, so the width lives in the
* filename. This is the assertion that reaches the derivatives named by a
* template literal, which the `srcSet` scan above cannot see.
*/
it('gives every ladder derivative the width in its filename', function (): void {
$wrong = [];
$checked = 0;

foreach (publicImageFiles() as $absolute) {
if (preg_match('#-(\d+)\.webp$#', $absolute, $match) !== 1) {
continue;
}

[$actual] = imageDimensions($absolute);
$checked++;

if ($actual !== (int) $match[1]) {
$name = basename($absolute);
$wrong[] = "{$name} is {$actual}px wide";
}
}

expect($wrong)->toBe([]);
expect($checked)->toBeGreaterThan(0);
});

it('shoots both themes of a pair at one size', function (): void {
$mismatched = [];
$checked = 0;

foreach (publicImageFiles() as $absolute) {
if (! str_contains($absolute, '-light')) {
continue;
}

$dark = str_replace('-light', '-dark', $absolute);

if (! is_file($dark)) {
continue;
}

$lightSize = imageDimensions($absolute);
$darkSize = imageDimensions($dark);
$checked++;

if ($lightSize !== $darkSize) {
$name = basename($absolute);
$mismatched[] = "{$name} is {$lightSize[0]}x{$lightSize[1]} but its dark variant is {$darkSize[0]}x{$darkSize[1]}";
}
}

expect($mismatched)->toBe([]);
expect($checked)->toBeGreaterThan(0);
});
Loading