Polish release notes and in-app help (#2070)

* feat(ui): improve changelog and help navigation

Make release notes easier to scan and reach from the app. Also keep the homepage marquee within the layout viewport so it cannot create horizontal scrolling.

* fix(website): reject duplicate release anchors
This commit is contained in:
Mohamed Boudra
2026-07-14 21:06:25 +02:00
committed by GitHub
parent cd6c608c9e
commit d781b68711
14 changed files with 310 additions and 110 deletions
@@ -84,7 +84,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
<PhoneShowcase />
{/* Content section */}
<div className="bg-background">
<div className="landing-content bg-background">
<main className="p-6 md:p-20 md:pt-40 max-w-5xl mx-auto">
<div className="space-y-24">
<SocialProofWall />
+83 -3
View File
@@ -4,6 +4,14 @@ import changelogMarkdown from "../../../../CHANGELOG.md?raw";
import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
interface ChangelogRelease {
version: string;
date: string;
markdown: string;
}
const releaseHeadingPattern = /^## (.+?) - (\d{4}-\d{2}-\d{2})$/;
export const Route = createFileRoute("/changelog")({
head: () =>
pageMeta(
@@ -14,12 +22,84 @@ export const Route = createFileRoute("/changelog")({
component: Changelog,
});
function formatDate(date: string): string {
const [year, month, day] = date.split("-").map(Number);
return new Intl.DateTimeFormat("en", {
month: "long",
day: "numeric",
year: "numeric",
timeZone: "UTC",
}).format(new Date(Date.UTC(year, month - 1, day)));
}
function parseChangelog(markdown: string): ChangelogRelease[] {
const releases: ChangelogRelease[] = [];
const versions = new Set<string>();
let currentRelease: ChangelogRelease | null = null;
for (const line of markdown.split("\n")) {
const heading = line.match(releaseHeadingPattern);
if (heading) {
const version = heading[1];
if (versions.has(version)) {
throw new Error(`Duplicate changelog version: ${version}`);
}
versions.add(version);
if (currentRelease) releases.push(currentRelease);
currentRelease = {
version,
date: heading[2],
markdown: "",
};
continue;
}
if (currentRelease) currentRelease.markdown += `${line}\n`;
}
if (currentRelease) releases.push(currentRelease);
return releases;
}
const changelogReleases = parseChangelog(changelogMarkdown);
function Release({ release }: { release: ChangelogRelease }) {
const anchor = `release-${release.version}`;
return (
<article className="changelog-release">
<time dateTime={release.date} className="changelog-release-date">
{formatDate(release.date)}
</time>
<div id={anchor} className="changelog-release-heading">
<a
href={`#${anchor}`}
className="changelog-heading-anchor"
aria-label={`Link to Paseo ${release.version}`}
>
<span aria-hidden="true">#</span>
</a>
<h2 className="changelog-release-title">
Paseo <span>{release.version}</span>
</h2>
</div>
<div className="changelog-release-notes">
<ReactMarkdown>{release.markdown}</ReactMarkdown>
</div>
</article>
);
}
function Changelog() {
return (
<SiteShell width="default">
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
</article>
<div className="max-w-2xl">
<h1 className="mb-12 text-3xl font-medium tracking-tight">Changelog</h1>
{changelogReleases.map((release) => (
<Release key={release.version} release={release} />
))}
</div>
</SiteShell>
);
}
+134 -45
View File
@@ -1,5 +1,31 @@
@import "tailwindcss";
* {
scrollbar-color: var(--color-scrollbar-handle) transparent;
scrollbar-width: thin;
}
*::-webkit-scrollbar {
width: 8px;
height: 8px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
border: 2px solid transparent;
border-radius: 9999px;
background: var(--color-scrollbar-handle) content-box;
}
*::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}
@keyframes voice-bar {
0% {
transform: scaleY(1);
@@ -19,11 +45,15 @@
}
.social-proof-marquee {
margin-inline: calc(50% - 50vw);
margin-inline: calc(50% - 50cqw);
overflow-x: clip;
position: relative;
}
.landing-content {
container-type: inline-size;
}
.social-proof-marquee::before,
.social-proof-marquee::after {
content: "";
@@ -93,66 +123,124 @@
font-size: 0.875em;
}
/* Markdown rendering styles for /changelog */
.changelog-markdown {
color: rgba(250, 250, 250, 0.82);
/* Changelog */
.changelog-release {
padding-bottom: 3rem;
}
.changelog-release + .changelog-release {
border-top: 1px solid var(--color-border);
padding-top: 3rem;
}
.changelog-release-date {
display: block;
margin-bottom: 0.625rem;
color: var(--color-muted-foreground);
font-size: 0.8125rem;
}
.changelog-release-heading {
position: relative;
scroll-margin-top: 1.5rem;
margin-bottom: 1.75rem;
}
.changelog-release-title {
color: var(--color-foreground);
font-size: 1.25rem;
font-weight: 500;
letter-spacing: -0.01em;
line-height: 1.4;
}
.changelog-release-title span {
color: var(--color-muted-foreground);
font-weight: 400;
}
.changelog-heading-anchor {
position: absolute;
top: 0;
right: 100%;
bottom: 0;
display: inline-flex;
align-items: center;
padding-right: 0.375rem;
color: color-mix(in srgb, var(--color-foreground) 40%, transparent);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 400;
text-decoration: none;
opacity: 0;
transition: opacity 150ms ease;
}
.changelog-release-heading:hover .changelog-heading-anchor,
.changelog-heading-anchor:focus {
opacity: 1;
}
.changelog-heading-anchor:hover {
color: var(--color-foreground);
}
.changelog-release-notes {
color: color-mix(in srgb, var(--color-foreground) 76%, transparent);
font-size: 0.9375rem;
line-height: 1.7;
}
.changelog-markdown h1 {
color: var(--color-foreground);
font-size: 2rem;
line-height: 1.2;
font-weight: 600;
margin-bottom: 1rem;
}
.changelog-markdown h2 {
color: var(--color-foreground);
font-size: 1.5rem;
line-height: 1.3;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--color-border);
}
.changelog-markdown h3 {
color: var(--color-foreground);
font-size: 1.125rem;
.changelog-release-notes h3 {
margin-top: 1.75rem;
margin-bottom: 0.625rem;
color: var(--color-muted-foreground);
font-size: 0.875rem;
font-weight: 500;
line-height: 1.4;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
.changelog-markdown p {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
.changelog-release-notes h3:first-child {
margin-top: 0;
}
.changelog-markdown ul {
list-style: disc;
margin-left: 1.25rem;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
.changelog-release-notes p {
margin-block: 0.75rem;
}
.changelog-release-notes ul {
display: grid;
gap: 0.5rem;
gap: 0.625rem;
margin-top: 0.75rem;
padding-left: 1.25rem;
list-style: disc;
}
.changelog-markdown li {
padding-left: 0.125rem;
.changelog-release-notes li::marker {
color: color-mix(in srgb, var(--color-primary) 65%, var(--color-muted-foreground));
}
.changelog-markdown a {
.changelog-release-notes strong {
color: var(--color-foreground);
text-decoration: underline;
text-underline-offset: 2px;
font-weight: 500;
}
.changelog-markdown a:hover {
color: rgba(250, 250, 250, 0.8);
.changelog-release-notes a {
color: var(--color-foreground);
text-decoration-color: color-mix(in srgb, var(--color-primary) 65%, transparent);
text-decoration-line: underline;
text-underline-offset: 3px;
}
.changelog-release-notes a:hover {
color: color-mix(in srgb, var(--color-foreground) 78%, var(--color-primary));
}
.changelog-release-notes code {
border-radius: 0.25rem;
background: var(--color-muted);
padding: 0.125rem 0.375rem;
color: var(--color-foreground);
font-size: 0.875em;
}
/* Docs page prose styles — mirrors the original docs/*.tsx components */
@@ -493,6 +581,7 @@
--color-muted-foreground: #a8adac;
--color-card: #171d1c;
--color-border: #252b2a;
--color-scrollbar-handle: #717574;
--color-primary: #239956;
--color-primary-foreground: #ffffff;
--color-secondary: #252b2a;