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
221 changes: 124 additions & 97 deletions src/components/Changelog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const tagStyles: Record<string, string> = {
improvement: 'border-blue-500/30 bg-blue-500/[0.08] text-blue-400',
fix: 'border-amber-500/30 bg-amber-500/[0.08] text-amber-400',
beta: 'border-purple-500/30 bg-purple-500/[0.08] text-purple-400',
pivot: 'border-rose-500/40 bg-rose-500/[0.10] text-rose-300',
};

const sectionDotColor: Record<string, string> = {
Expand All @@ -18,7 +19,18 @@ const sectionDotColor: Record<string, string> = {
fix: 'bg-amber-400',
};

function EntryCard({ entry, index }: { entry: ChangelogEntry; index: number }) {
/**
* One changelog release, laid out as a vertical timeline row — Mintlify-style:
* a sticky version label on the left rail, a node on the connecting line, and
* the release content to the right.
*/
function TimelineEntry({
entry,
index,
}: {
entry: ChangelogEntry;
index: number;
}) {
const [expandedSections, setExpandedSections] = useState<
Record<number, boolean>
>({});
Expand All @@ -27,112 +39,127 @@ function EntryCard({ entry, index }: { entry: ChangelogEntry; index: number }) {
setExpandedSections(prev => ({ ...prev, [i]: !prev[i] }));
};

const isPivot = entry.tag === 'pivot';

return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, delay: index * 0.07 }}
className="rounded-2xl border border-white/[0.10] bg-white/[0.04] backdrop-blur-sm overflow-hidden"
className="flex flex-col gap-3 sm:flex-row sm:gap-9"
>
{/* Card header */}
<div className="px-8 pt-8 pb-6 border-b border-white/[0.06]">
<div className="flex flex-wrap items-center gap-3 mb-3">
<span className="font-mono text-sm text-neutral-500 tracking-wide">
{entry.version}
</span>
<span className="text-neutral-700 text-xs">·</span>
<span className="font-mono text-sm text-neutral-500 tracking-wide">
{entry.date}
</span>
{entry.tag && (
<span
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest ${tagStyles[entry.tag]}`}
>
{entry.tag}
</span>
)}
{/* Left rail — version, date, tag (sticky on desktop) */}
<div className="sm:sticky sm:top-28 sm:w-32 sm:shrink-0 sm:self-start sm:pt-0.5 sm:text-right">
<div className="font-mono text-lg font-semibold tracking-tight text-white">
{entry.version}
</div>
<h2 className="text-2xl font-bold text-white tracking-tight leading-snug">
{entry.title}
</h2>
<div className="mt-1 font-mono text-xs tracking-wide text-neutral-500">
{entry.date}
</div>
{entry.tag && (
<span
className={`mt-2.5 inline-flex items-center rounded-full border px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest ${tagStyles[entry.tag]}`}
>
{entry.tag}
</span>
)}
</div>

{/* Sections */}
<div className="px-8 py-6 space-y-7">
{entry.sections.map((section, si) => {
const isCollapsible = section.collapsible;
const isExpanded = expandedSections[si] ?? false;
{/* Connecting line + node + release content */}
<div
className={`relative flex-1 border-l pb-14 pl-6 sm:pl-9 ${
isPivot ? 'border-rose-500/25' : 'border-white/[0.10]'
}`}
>
<span
className={`absolute -left-[5px] top-2 h-2.5 w-2.5 rounded-full ${
isPivot
? 'bg-rose-400 shadow-[0_0_0_4px_rgba(244,63,94,0.12)]'
: 'bg-neutral-500'
}`}
aria-hidden
/>

<h2 className="text-xl font-bold leading-snug tracking-tight text-white sm:text-2xl">
{entry.title}
</h2>

return (
<div key={si}>
{isCollapsible ? (
/* Collapsible section */
<div>
<button
onClick={() => toggle(si)}
className="flex items-center gap-1.5 text-xs font-medium text-neutral-500 hover:text-neutral-300 transition-colors mb-0 group"
>
<ChevronRight
className={`w-3.5 h-3.5 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
/>
<span>
{isExpanded
? section.label
: `${section.label} / Housekeeping...`}
</span>
</button>
<div className="mt-6 space-y-7">
{entry.sections.map((section, si) => {
const isCollapsible = section.collapsible;
const isExpanded = expandedSections[si] ?? false;

{isExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="mt-4 pl-5 border-l border-white/[0.06] space-y-4"
return (
<div key={si}>
{isCollapsible ? (
/* Collapsible section */
<div>
<button
onClick={() => toggle(si)}
className="group mb-0 flex items-center gap-1.5 text-xs font-medium text-neutral-500 transition-colors hover:text-neutral-300"
Comment on lines +97 to +99
>
<ChevronRight
className={`h-3.5 w-3.5 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
/>
<span>
{isExpanded
? section.label
: `${section.label} / Housekeeping...`}
</span>
</button>
Comment on lines +97 to +109
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add ARIA state/association to collapsible sections.

The toggle button should expose expansion state and control target (aria-expanded, aria-controls) so screen readers can interpret the section behavior.

Suggested patch
 {entry.sections.map((section, si) => {
   const isCollapsible = section.collapsible;
   const isExpanded = expandedSections[si] ?? false;
+  const panelId = `section-panel-${entry.version}-${si}`;
+  const buttonId = `section-button-${entry.version}-${si}`;

   return (
     <div key={si}>
       {isCollapsible ? (
         /* Collapsible section */
         <div>
           <button
+            id={buttonId}
+            aria-expanded={isExpanded}
+            aria-controls={panelId}
             onClick={() => toggle(si)}
             className="group mb-0 flex items-center gap-1.5 text-xs font-medium text-neutral-500 transition-colors hover:text-neutral-300"
           >
...
           {isExpanded && (
             <motion.div
+              id={panelId}
+              role="region"
+              aria-labelledby={buttonId}
               initial={{ opacity: 0, height: 0 }}
               animate={{ opacity: 1, height: 'auto' }}
               exit={{ opacity: 0, height: 0 }}

Also applies to: 111-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Changelog.tsx` around lines 97 - 109, The collapse toggle
button doesn't expose ARIA state/association; update the button rendered in
Changelog (the one using toggle(si), ChevronRight, isExpanded, and
section.label) to include aria-expanded={isExpanded} and aria-controls pointing
to the collapsible panel's id, and ensure the corresponding collapsible panel
element (the section content rendered elsewhere for this section) has that same
unique id and an appropriate role/aria-labelledby or role="region" for screen
readers to associate the control with its panel; keep the existing toggle(si)
behavior and generated id strategy consistent (e.g., derive id from
section.id/si) so both button and panel reference the same identifier.


{isExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="mt-4 space-y-4 border-l border-white/[0.06] pl-5"
>
{section.items.map((item, ii) => (
<p
key={ii}
className="text-sm leading-relaxed text-neutral-400"
>
<span className="font-semibold text-neutral-200">
{item.label}:
</span>{' '}
{item.description}
</p>
))}
</motion.div>
)}
</div>
) : (
/* Normal section */
<div>
<div className="mb-4 flex items-center gap-2">
<span
className={`h-1.5 w-1.5 shrink-0 rounded-full ${sectionDotColor[section.type] ?? 'bg-neutral-500'}`}
/>
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">
{section.label}
</p>
</div>
<ul className="space-y-3.5">
{section.items.map((item, ii) => (
<p
key={ii}
className="text-sm text-neutral-400 leading-relaxed"
>
<span className="font-semibold text-neutral-200">
{item.label}:
</span>{' '}
{item.description}
</p>
<li key={ii} className="flex items-start gap-3 text-sm">
<span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-neutral-600" />
<p className="leading-relaxed text-neutral-400">
<span className="font-semibold text-neutral-200">
{item.label}:
</span>{' '}
{item.description}
</p>
</li>
))}
</motion.div>
)}
</div>
) : (
/* Normal section */
<div>
<div className="flex items-center gap-2 mb-4">
<span
className={`h-1.5 w-1.5 rounded-full shrink-0 ${sectionDotColor[section.type] ?? 'bg-neutral-500'}`}
/>
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">
{section.label}
</p>
</ul>
</div>
<ul className="space-y-3.5">
{section.items.map((item, ii) => (
<li key={ii} className="flex items-start gap-3 text-sm">
<span className="mt-[7px] h-1 w-1 shrink-0 rounded-full bg-neutral-600" />
<p className="text-neutral-400 leading-relaxed">
<span className="font-semibold text-neutral-200">
{item.label}:
</span>{' '}
{item.description}
</p>
</li>
))}
</ul>
</div>
)}
</div>
);
})}
)}
</div>
);
})}
</div>
</div>
</motion.div>
);
Expand Down Expand Up @@ -222,7 +249,7 @@ const Changelog: React.FC = () => {
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
className="flex gap-1 mb-10 p-1 rounded-xl bg-white/[0.04] border border-white/[0.08] w-fit"
className="flex gap-1 mb-12 p-1 rounded-xl bg-white/[0.04] border border-white/[0.08] w-fit"
>
{(['web', 'cli'] as Tab[]).map(t => (
<button
Expand All @@ -239,15 +266,15 @@ const Changelog: React.FC = () => {
))}
</motion.div>

{/* Entries */}
<div className="space-y-4">
{/* Entries — vertical timeline */}
<div className="relative">
{entries.map((entry, i) => (
<EntryCard key={entry.version} entry={entry} index={i} />
<TimelineEntry key={entry.version} entry={entry} index={i} />
))}
</div>

{/* Footer */}
<div className="mt-16 pt-8 border-t border-white/[0.06] text-center">
<div className="mt-8 pt-8 border-t border-white/[0.06] text-center">
<p className="text-sm text-neutral-400">
Questions or feedback?{' '}
<a
Expand Down
31 changes: 13 additions & 18 deletions src/components/TestimonialsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,11 @@ function TestimonialMarqueeColumn({
return (
<div className={`${heightClass} overflow-hidden`}>
<div
className="pointer-events-none absolute inset-x-0 top-0 z-[2] h-14 bg-gradient-to-b from-[#f3f3f2] via-[#f3f3f2]/90 to-transparent sm:h-16"
className="pointer-events-none absolute inset-x-0 top-0 z-[2] h-14 bg-gradient-to-b from-[#161618] via-[#161618]/90 to-transparent sm:h-16"
aria-hidden
/>
<div
className="pointer-events-none absolute inset-x-0 bottom-0 z-[2] h-14 bg-gradient-to-t from-[#f3f3f2] via-[#f3f3f2]/90 to-transparent sm:h-16"
className="pointer-events-none absolute inset-x-0 bottom-0 z-[2] h-14 bg-gradient-to-t from-[#161618] via-[#161618]/90 to-transparent sm:h-16"
aria-hidden
/>

Expand Down Expand Up @@ -249,31 +249,26 @@ const TestimonialsSection: React.FC = () => {
return (
<section
id="testimonials"
className="relative w-full overflow-hidden bg-[#f3f3f2] py-24 lg:py-28 text-neutral-900 antialiased font-space"
className="relative w-full overflow-hidden bg-[#161618] py-24 lg:py-28 text-neutral-200 antialiased font-space"
Comment on lines 250 to +252
aria-labelledby="testimonials-heading"
>
{/* Minimal editorial backdrop — fine grid + a soft neutral vignette */}
<div aria-hidden className="pointer-events-none absolute inset-0 z-[1]">
<div className="absolute inset-0 bg-[linear-gradient(rgba(24,24,27,0.028)_1px,transparent_1px),linear-gradient(90deg,rgba(24,24,27,0.028)_1px,transparent_1px)] bg-[length:44px_44px]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_72%_52%_at_50%_-18%,rgba(24,24,27,0.04),transparent_58%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_85%_55%_at_50%_108%,rgba(24,24,27,0.035),transparent_52%)]" />
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:44px_44px]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_72%_52%_at_50%_-18%,rgba(255,255,255,0.035),transparent_58%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_85%_55%_at_50%_108%,rgba(255,255,255,0.03),transparent_52%)]" />
</div>

{/* Soft edge fades — blend the section into the black sections above and
below. z-[5] keeps them under the content (z-10) so only the
background edge softens, never the heading or cards. */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-20 h-32 bg-gradient-to-b from-black to-transparent sm:h-36 lg:h-44"
className="pointer-events-none absolute inset-x-0 top-0 z-[5] h-24 bg-gradient-to-b from-black to-transparent sm:h-28"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-36 bg-gradient-to-t from-black to-transparent sm:h-40 lg:h-48"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 left-0 z-20 w-14 bg-gradient-to-r from-black to-transparent sm:w-20 lg:w-32"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 z-20 w-14 bg-gradient-to-l from-black to-transparent sm:w-20 lg:w-32"
className="pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-24 bg-gradient-to-t from-black to-transparent sm:h-28"
/>

<div className="relative z-10 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
Expand All @@ -286,11 +281,11 @@ const TestimonialsSection: React.FC = () => {
>
<h2
id="testimonials-heading"
className="text-4xl font-semibold tracking-tight text-neutral-950 sm:text-5xl lg:text-[3.25rem] leading-[1.12]"
className="text-4xl font-semibold tracking-tight text-white sm:text-5xl lg:text-[3.25rem] leading-[1.12]"
>
Builders love Refactron.
</h2>
<p className="mt-4 text-base text-neutral-600 sm:text-lg leading-relaxed">
<p className="mt-4 text-base text-neutral-400 sm:text-lg leading-relaxed">
And they can&apos;t stop talking about safer refactors and boring,
reviewable diffs.
</p>
Expand Down
Loading
Loading