feat(web): redesign and polish vinext.dev #2587
Open
NathanDrake2406 wants to merge 43 commits into
Open
Conversation
commit: |
Contributor
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
NathanDrake2406
marked this pull request as draft
July 10, 2026 16:45
NathanDrake2406
marked this pull request as ready for review
July 10, 2026 17:10
NathanDrake2406
marked this pull request as draft
July 10, 2026 17:15
NathanDrake2406
marked this pull request as ready for review
July 10, 2026 17:47
NathanDrake2406
marked this pull request as draft
July 10, 2026 19:01
NathanDrake2406
marked this pull request as ready for review
July 10, 2026 19:24
NathanDrake2406
marked this pull request as draft
July 11, 2026 16:53
Contributor
Author
|
@hyoban could you please share your impressions on this? I've tuned it pretty extensively but it's still quite not there yet imho and I'm struggling to articulate why |
The web app still presented a Kumo-oriented landing page and inconsistent dashboard surfaces, which obscured vinext's Vite, Cloudflare, performance, and compatibility story. Replace those surfaces with a unified landing experience, responsive shared chrome, data-backed benchmark and compatibility views, Cloudflare Images support messaging, and traffic-aware pre-rendering discovery. Keep footer navigation compact and make the supported capability matrix reflect the deployed runtime. Validation: vp check apps/web/app tests/performance-dashboard-components.test.ts; vp test run tests/performance-dashboard-components.test.ts Co-authored-by: MaxtuneLee <60775796+MaxtuneLee@users.noreply.github.com>
…ling The engine-swap plate font used a clamp window (667-1267px) offset from the section headings' window (593-1185px), so the plate and headings grew at different viewport ranges and the composition looked inconsistent between breakpoints. The get-started CTA card spanned the full 1180px container regardless of viewport while its content only filled a third of it, and landing sections mixed off-grid spacing values (13/14/17/18/22/26/38px) with the dominant 8-based scale. On the benchmarks page, wide byte ticks like "115.6 KB" overlapped the rotated "bytes" axis label because the 70px left padding only fit ms ticks. Plate font is now clamp(38px,6.4vw,76px) - exactly 1.1875x the section h2 (5.4vw), so both hit min/max at the same viewports and keep a constant ratio while fluid. The CTA card caps at the same 460px rail as the command box above it, so the two cards share edges at every width. All landing gaps/paddings/margins snap to the 4/8px grid, and chart left padding widens to 90px to clear the widest byte tick. Also folds in landing motion/site chrome cleanups from the redesign pass on this branch.
The footer only linked Blog and npm while the Discord community was reachable solely from the get-started card. Adds the Cloudflare Discord server invite (not a channel deep link, which only resolves for existing members) alphabetically between Blog and npm, and snaps the footer's off-grid spacing (30/60/14px) to the 4/8px scale. Also commits the pnpm-lock prune from the redesign's dependency removals (kumo, heroicons, mdx-loader).
The benchmarks dashboard rendered run timestamps with Date.toLocaleString()/toLocaleDateString() inside client components. Those calls use the runtime's locale and timezone, so the Workers SSR pass and the browser produced different text whenever the viewer's timezone or locale differed from the server's, and React failed hydration and regenerated the tree on every visit. Timestamps now format through shared helpers pinned to en-US and UTC (labelled "UTC"), so both render passes emit identical text.
The race animation compressed both build bars into a fixed 2.8s playback with an ease-out curve, so the on-screen counters ticked faster than wall-clock seconds and the "watch the actual wait" story did not hold. The animation now runs linearly at the measured Next.js build duration, so a second on screen is a build second and the vinext bar finishes at its real median. Playback is capped at 5s and compresses proportionally beyond it, so a slow ingest or a heavier benchmark suite cannot drag the animation past attention span.
The landing page computed its benchmark copy as nextjs/vinext and
rendered "{multiple} faster build time" and "~{pct}% smaller client
bundle" unconditionally. The numbers are live D1 medians, so a vinext
regression would render nonsense like "0.9x faster" or "~-12% smaller"
instead of admitting the loss.
Separately, reveal elements ship with inline opacity:0 and are only
unhidden by the client motion controller. The CSS rule pinning them
visible was accidentally scoped inside prefers-reduced-motion, so a
normal no-JS visit rendered the hero and sections invisible.
Copy now derives from compareBuild/compareBundle, which return a
verdict (better/worse/par) plus winner-relative magnitude, and the
headline/stat captions switch on the verdict ("slower", "larger",
parity wording). The head theme script stamps html.js synchronously
and reveal elements are pinned visible for html:not(.js), so the
server render fails open when JS never runs.
The benchmark animation used Next.js as both its duration and bar denominator. When vinext was slower, the race ended early and rendered an overfull vinext bar. Derive both rows from the longest build, share the same frame calculation with reduced motion, and cover slower-vinext and equal-duration races. Keep landing-stat-only helpers private so Knip remains clean.
Landing benchmark and compatibility figures were presented without enough scope or fallback provenance, while motion hid server-rendered content until hydration completed. Carry live versus reference provenance through the landing stats boundary, qualify benchmark and migration copy, render completed content by default, and opt into animation state only after the client controller mounts. Keep race arithmetic in a typed pure helper covered by focused regression tests.
The landing page previously kept all section markup in one 3,000-line component and coordinated every animation through one stringly-typed controller. That made isolated review and lifecycle ownership difficult even though the rendered experience was stable. Extract the unchanged section markup into purpose-specific server components. Replace the controller class with typed, section-scoped hooks, IntersectionObserver triggers, and explicit cleanup while preserving the existing SSR, reduced-motion, animation, and responsive behavior.
Model repeated compatibility and deploy rows as typed data and move the static Vite artwork to an asset. Calculate build comparisons from raw values, show race completion symmetrically, and leave static color tokens owned by CSS.
The engine-swap paragraph buried the vinext check command inside a run-on sentence, and the get-started card duplicated GitHub/Discord links already in the header and footer while wrapping a plain closing line in a heavy bordered/gradient card that fought the command box above it for attention. Split the vinext check callout into its own line, corrected the command to `npx vinext check`, dropped the redundant get-started card chrome and duplicate links (keep only Quick start), and swapped the flat "33-route benchmark snapshot." fallback headline for "Built for speed." to read as marketing copy instead of a data-source label.
Chrome nav icons (benchmarks/compatibility/github) sit under the 44px touch-target minimum with no compensating hit-area expansion, unlike the theme-toggle and copy buttons beside them in the same row. Below that size mobile taps land on the wrong link or miss it. Join .chrome-link into the existing -8px inset ::after pattern already used by .theme-toggle/[data-copy], and give it position:relative so the pseudo-element anchors to it. Also remove aria-live="polite" from the copy button in get-started-section.tsx: its aria-label is already rewritten on every state change, so the live region was announcing nothing extra.
Sun/moon glyph swap on theme toggle used display:none/block, an un-animatable property, so it hard-cut while the toggle's color and lift transition around it and the page ground crossfades. Felt broken next to the rest of the polish. display can't transition. Swap opacity+rotate (transform-box: fill-box so the rotation is centered on each icon) matching the toggle's existing 0.16-0.18s ease-out.
The race bar's playback duration was capped at min(longest, 5)s. The shipped fallback build times (3.1s vinext / 6.2s nextjs) exceed that, so the bar hit the finish line at 5 real seconds while still reading "6.2s" — a real mismatch between the number shown and time elapsed, in a demo whose whole point is that the numbers are honest. Cap was doing UX-trim duty it was never meant for. Raise it to 10s, a defensive ceiling against a bad/outlier data point rather than a speed-up for realistic build times, so playback is true 1:1 wall-clock for any normal measurement. Added a regression test pinning the 6.2s fallback to real-time duration, plus a case proving the 10s ceiling still compresses actual outliers.
Assertion still checked for "33-route benchmark snapshot.", the pre-rewrite fallback headline. Commit a8ecbe6 changed the real copy to "33-route dynamic-render benchmark · reference snapshot · live benchmark data unavailable" but missed this one assertion, so CI broke on a stale string. Test itself is correct and stays: it's the only check that fallback figures get labeled as fallback instead of masquerading as a live measurement. Updated the expected substring to match current copy.
… fixed px Logos in the hero strip, deploy table, and nav chrome were sized in raw pixels, decoupled from the text they sit next to. Bumping them up meant hand-picking new px values with no relationship to the row's type scale, and any future font-size tweak would silently throw the icons out of proportion. Switched every logo/glyph to em, sized off the font-size of the row it lives in, so the icons track the text instead of drifting from it. Also fixed a bug this introduced in the deploy table: the AWS lambda glyph had its box width and its font-size bump on the same element, so the box's em resolved against the bumped font-size instead of the row's, making that row's box ~30% wider and pushing "AWS Lambda" out of alignment with the other rows. Split the box (sized off the row) from the glyph (sized off the box) onto separate elements to fix it.
Deploy-section globe pins used a rgba(0,0,0,.6) drop shadow and landing sections padded to 64px top/bottom — both read as harsh/cramped next to an agency-tier design audit, and the compatibility cards were flat single-radius boxes with no depth cue. --pin-shadow was tuned for contrast against the globe art, not for restraint; sections were never revisited after early layout passes. Soften --pin-shadow to rgba(0,0,0,.22), bump deploy/benchmark/compat padding to 96px and get-started to 96px/120px, and give CompatibilityGroupCard a nested outer-shell/inner-core (double-bezel) structure with a concentric radius and inset highlight. Skipped the matching button-in-button CTA treatment: no boxed CTA button exists on this branch anymore (removed by earlier decluttering commits), and the only matching CSS (.primary-action/.action-arrow) is dead. Retrofitting it means adding UI that was deliberately removed.
Landing motion mixed layout-heavy updates, blanket reduced-motion overrides, and abrupt chart and dialog state changes. Vercel and Deno Deploy marks also inherited the SVG default black fill, making them hard to read on the dark landing surface. Cache race elements, move bar progression to WAAPI, keep reduced-motion changes live, and make chart, dialog, payoff, and deploy-pin transitions interruptible and theme-aware. Preserve the original engine-width contraction by design, and let inline deployment logos inherit the row color in both themes.
The speed race and engine swap restarted whenever landing motion remounted, so navigating away and returning replayed long explanatory sequences. Persist a sessionStorage marker when each sequence actually starts. Returning in the same tab now renders the final state immediately, while a new tab or browser session still gets one play and restricted storage safely degrades to replaying.
…utter In light mode the globe rendered as a flat burnt-orange map cutout with a washed peach halo, and sat ~96px further from the deploy table than intended. The dark theme's additive glow supplies both the sphere's light-to-shade ramp and its atmosphere; on paper an additive wash can only push toward white, so the halo read as a stain and the uniformly filtered dots lost all form. The benchmark payoff also rendered "185 KB →125 KB": the U+00A0 after the arrow sat at end of line in JSX, where the transform trims it with the line break. Fix: build the ramp in layers instead of one filter. The base <img> is the lit pass; ::after stacks a darkened desaturated copy masked away from the light source (generated-gradient mask, so the url() mask CORS trap does not apply); ::before becomes a multiply apricot/terracotta wash whose mask ends before the section's overflow:hidden edge to avoid a straight seam. --globe-column drops 576px -> 480px to absorb the ~96px of transparent left padding baked into globe.svg. The arrow nbsp moves into an explicit expression so JSX cannot trim it. Verified in both themes at 1440px and 1000px; dark mode output unchanged apart from the tighter gutter.
The landing page's one ask, npx vinext init, first appeared ~5,000px down in the get-started section, so a visitor had to sit through the full scroll story before they could act. The hero spent its last slot on a "deploys anywhere" logo strip that duplicates the deploy section, and there was no path at all for the growing share of devs who migrate by handing the task to a coding agent. Hero: the logo strip becomes a quiet copy pill ($ npx vinext init) in the same reveal slot, reusing the get-started box styling and [data-copy] contract. Get-started: a twin box under the command box carries a migration prompt for coding agents. The prompt lives only in the data-copy payload and mirrors the flow the repo recommends (the official migrate-to-vinext Agent Skill first, then check -> init -> dev:vinext -> build:vinext), stating what init does and does not touch so an agent doesn't "fix" the CJS renames or "type": "module". The copy controller previously reset every button to the hardcoded label "copy"; it now restores each button's own idle text and aria-label, and blur-dissolves the label swap (WAAPI, reduced-motion skipped) instead of hard-cutting. Also removes dead hooks: .primary-action/.action-arrow CSS had no users, and the motion controller looked up "ready"/"scrollHint" elements that no longer exist in the DOM. 15/15 landing tests pass; both themes verified at 1440px.
Copy controls now morph between copy, copied, and fallback states without replacing their DOM content. The previous text mutation could not support stable icon transitions across the shared landing-page controls.\n\nAdd a compact reusable control that preserves the existing clipboard event boundary and exposes outcome state through data attributes.
Past the hero, heroBg faded to zero on scroll and every section below sat on flat --bg, so the lower half read as centered slides in a black void with no depth or continuity between them. Add a page-height layer behind the z-2 content that never fades: four soft warm/cool glows placed down the page so each section scrolls through its own pool of light and the hero's warm/cool rhythm keeps going, plus a masked low-opacity dot field so the surface reads as designed rather than empty. Pure CSS and token-driven, so it survives reduced-motion, no-JS, and background-tab loads that freeze the rAF hero, and it adapts across themes.
The benchmark section stacked a headline over full-width race bars, the same shape most other sections use, and the bars ran the whole 1180px container so the vinext-vs-Next.js gap was spread too wide to read at a glance. Split it into a claim rail (headline + provenance) beside the race bars and payoff stats, matching the narrative-left / visual-right composition the deploy section already uses. The bars now sit in a constrained column so the speed difference reads immediately, and the page alternates composition instead of repeating one template down the scroll. Collapses to a single column under 860px. Every data-el hook is unchanged, so the race animation is untouched.
The client-bundle stat rendered as "~32%", hedging a figure that comes straight from the benchmark snapshot. Show the exact "32%".
Date and metric range text was wrapping onto multiple lines in narrow columns, breaking the table layout. Added whitespace-nowrap to table headers, the latest-run date badge, and mobile metric ranges.
Benchmark tables break lines inside formatted tokens: the range column splits "128.5 KB" across lines and run dates wrap mid-date in narrow cells. Plain spaces inside a single value are not valid break points. The formatters emitted breakable spaces between number and unit and between date parts. They now join with non-breaking spaces, so every caller renders each value or date as one atomic token while surrounding text still wraps at real separators.
Every data point in every series was its own link to the same commit URL: three commits with two series produced six identical tab stops, and point values were only reachable by hover, invisible to keyboard and screen-reader users. Points are now decorative; each commit gets a single column-wide link whose accessible name carries every visible series value, with a combined tooltip shown on hover and keyboard focus.
The hero <h1> wrapped its two lines in <div> motion hooks, but h1 permits phrasing content only, so browsers may re-parent the divs and break the heading structure. The wrappers are now display:block spans; the data-el motion selectors are tag-agnostic.
The 1x56px divider between the payoff stats is a flex item, so any viewport narrow enough to wrap the row strands it as a dead tick on its own line. A fixed-height rule cannot follow flex wrapping; the 40px column gap already separates the stats, so the divider is removed.
The table dialog set an unconditional flex class on the <dialog> element, overriding the UA's dialog:not([open]) display:none. The closed dialog rendered as an invisible (opacity 0) full-size overlay, so the router tabs and everything under it ignored clicks. Display now stays UA-controlled; the flex column layout moves to a wrapper inside the dialog.
…ghosts Light mode reused text-contrast tokens (tuned for 4.5:1) as 12px dot fills, which rendered the grid muddy: olive partials next to warm-gray skips, salmon-adjacent fails, and a dull teal field. The unsupported dots also sat at 2:1 against the surface, under the 3:1 non-text floor. Dot fills and text colors have different jobs, so light mode gets dot-only tokens derived in OKLCH from the dark palette's hue anchors: each hue near its max sRGB chroma at a hue-appropriate depth, every fill at or above 3:1, with pass shifted to leaf green (148deg) because teal-emerald has no chroma headroom and pass dominates the field. Unsupported dots keep the deliberately faint fill but gain a --mute outline (grid, legend, and dialog swatches) so they stay findable. Dark mode aliases the existing tokens and is visually unchanged.
A dev environment with an empty (or hand-seeded 10-file demo) local D1 renders /compatibility as a blank shell, so grid density, colors, and the trend chart cannot be inspected locally. In development only, when the database is empty or holds the demo run, the page substitutes a captured snapshot of vinext.dev/compatibility (799 cells and 74 trend runs extracted from its RSC payload) and shows a banner naming the substitution. Production behavior is unchanged: real data, or the real empty/error state.
NathanDrake2406
force-pushed
the
vinext.dev
branch
from
July 18, 2026 07:01
92b479a to
a03ef1a
Compare
…empty dev" This reverts commit a03ef1a.
An unmatched URL currently renders Next's unstyled default 404 — bare sans-serif on white, no site chrome, no way back. Every mistyped or stale link drops visitors out of the brand entirely. The app never defined app/not-found.tsx, so the framework fallback wins. Add one: terminal-styled "vinext resolve" error card, display headline, and links to /, /compatibility, and /benchmarks. It renders inside the root layout, so chrome, footer, and both themes apply without extra work.
Every page currently titles itself "vinext" — tabs, history, and search
results cannot tell /benchmarks from /compatibility — and shared links
unfurl as bare text because no Open Graph metadata or image exists.
The root layout exported a single static title and nothing else. Switch it
to a title template ("%s · vinext") with per-page titles and descriptions
on /benchmarks, /compatibility, and the 404 page, and add openGraph +
twitter metadata pointing at a static 1200x630 card (public/img/og.png:
hero headline with Vite's brand gradient, Cloudflare mark beside the
wordmark, npx vinext init chip). metadataBase makes the image URL absolute,
which Slack/Discord/X require before they render a card.
A static PNG is deliberate over next/og ImageResponse: this site runs on
vinext's own runtime, and a file in public/ cannot fail there.
The landing page scrolled a "Same structure / New engine" outline-text marquee between the hero and the engine-swap section, with its own scroll-velocity skew in the motion loop and per-theme CSS overrides. It restated what the hero and engine plate already say and added a full viewport-width band of decoration between the pitch and the proof. Remove the component and its wiring: the "mq" element id, the marquee parallax/skew block in the rAF loop, and the reduced-motion and light-theme rules that existed only to tame it. Hero now hands straight into the engine-swap section.
Navigating to a fragment URL (vinext.dev/#compat, #deploy, #start) scrolls the section flush to the viewport top, putting its first line under the fixed chrome and its 96px legibility scrim. Sections carried no scroll-margin. Give every id-bearing section scroll-margin-top: 84px so fragment navigation lands the heading below the chrome.
Three motion gaps: compatibility grid dots teleport to new positions when the router filter changes while the adjacent line chart morphs over 260ms, the selected-tab underline pops between tabs with no bridge, and the 404 terminal card renders all output at once. The grid inconsistency reads as a bug — two visualisations reacting to the same control move differently. Grid dots now transition x/y with the same 260ms ease-in-out the chart uses for cx/cy, so both surfaces answer the filter as one. The tab underline lives on every tab scaled to zero and grows in on selection (200ms ease-out). The 404 card prints its three lines with a 90ms stagger to match the CLI framing; reduced-motion swaps to an opacity-only fade and disables the geometry transitions, following the existing overrides.
The landing claims test expected "Jul 10, 2026, 5:00 PM UTC" with plain spaces, so it failed on every run — locally and in CI — regardless of timezone. formatUtcDateTime has joined formatted dates with non-breaking spaces (U+00A0) since 768d520 so values never wrap mid-token; the assertion was never updated. It now uses explicit escapes so the non-breaking contract stays visible in the test source. Also give knip an apps/web workspace config. Without one, knip fell back to defaults and scanned a stale local apps/web/dist build (the root .gitignore's negated dist patterns defeat knip's gitignore handling), failing pre-commit with 92 "unused" build artifacts whenever dist exists locally. CI never saw this because it never has a dist directory. Entries follow the app's routing conventions; tailwindcss is ignored as a dependency because its only import is from CSS, which knip cannot trace.
The hero previously sat on gradient blobs alone; the new theme-specific constellation artwork (AVIF q85, 2560w, 178/119 KB) gives it a textured ground. The image URLs live only inside a min-width:940px media query, so phones never download them, and only the active theme's file is fetched. Wiring choices that matter: the entrance transition is gated on image.decode() from the hero motion module, so the settle never plays over a half-loaded image; the opposite theme's file is prefetched during idle time so a theme toggle swaps from cache instead of popping; and the artwork participates in the scroll choreography with a small parallax lift+zoom written per-frame by the existing rAF loop. The wrapper div owns the one-shot entrance transform and the inner div owns the continuous drift, so the two never retarget each other. Reduced motion inherits the existing heroBg hiding, which also skips the image fetch entirely.
…tion Scrolling off the hero left a long stretch where its text had already faded but "Start with the app you have." was still ~80vh away, so the reader traversed empty atmosphere with nothing arriving. The hero's exit text completes early in its runway, which made the remaining runway plus the swap section's centered pre-roll read as dead space. Trim the hero from 125vh to 115vh and pull the swap section up 15vh so its heading starts sliding in under the tail of the hero fade. The hero exit choreography is offsetHeight-based, so it adapts to the shorter runway without code changes.
The copyable "prompt for your coding agent" box on the get-started section carried a verbose, step-numbered prompt that duplicated the migrate-to-vinext Agent Skill's own instructions instead of just pointing an agent at it. Replace it with a shorter prompt that leads with the skill install, falls back to the vinext CLI (check, then init) with docs/web search as backup, and asks the agent to verify build/run/tests — trusting the skill and CLI output instead of re-deriving the steps inline.
NathanDrake2406
marked this pull request as ready for review
July 18, 2026 11:14
Contributor
Author
|
@james-elicx done. I've also folded ur change yesterday in |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2427