Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,6 @@ config.

# allow required generated files
!app/icons.gen.ts
!app/overrides.gen.css
!app/overrides.gen.css

public/releases/releases.json
25 changes: 7 additions & 18 deletions app/releases/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import { getReleases } from "@/lib/releases";
import { ReleaseCard } from "@/components/Releases/ReleaseCard";
import ReleasesClient from "@/components/Releases/ReleasesClient";

export default async function ReleasesPage() {
const releases = await getReleases();

return (
<div className="max-w-5xl mx-auto px-4 py-10 space-y-6">
<div>
<h1 className="text-3xl font-bold text-[#50B78B]">
Releases
</h1>
<p className="text-sm text-zinc-500 mt-1">
Celebrating contributors across CircuitVerse releases
</p>
</div>
<div className="mx-auto w-full max-w-5xl px-4 py-10">
<h1 className="text-3xl font-bold text-[#50B78B]">Releases</h1>
<p className="text-sm text-zinc-500 mt-1">
Celebrating contributors across CircuitVerse releases
</p>

<div className="space-y-4">
{releases.map((release, index) => (
<ReleaseCard
key={`${release.repoSlug}-${release.version}`}
release={release}
/>
))}
</div>
<ReleasesClient releases={releases} />
</div>
);
}
27 changes: 14 additions & 13 deletions components/Releases/ReleaseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,25 @@ export function ReleaseCard({ release }: { release: Release }) {
<Card className="transition-all hover:shadow-md hover:border-[#50B78B]/50">
<CardContent className="p-5 space-y-4">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-lg font-semibold">
{release.repo} — {release.version}
</h3>
<p className="text-sm text-muted-foreground">
{release.summary}
</p>
</div>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 sm:gap-4">
<div className="min-w-0">
<h3 className="text-base sm:text-lg font-semibold break-words">
{release.repo} — {release.version}
</h3>
<p className="text-sm text-muted-foreground break-words">
{release.summary}
</p>

<span className="text-sm text-zinc-500 whitespace-nowrap">
{release.date}
</span>
</div>
<span className="text-xs sm:text-sm text-zinc-500 whitespace-normal sm:whitespace-nowrap sm:self-start">
{release.date}
</span>

</div>

{/* Contributors */}
{release.contributors.length > 0 && (
<div className="flex flex-wrap gap-3 pt-2">
<div className="flex flex-wrap gap-2 pt-2">
{release.contributors.map((c) => (
<Link
key={c.username}
Expand Down
61 changes: 61 additions & 0 deletions components/Releases/ReleasesClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { useState } from "react";
import { Release } from "@/lib/releases";
import { ReleaseCard } from "./ReleaseCard";

export default function ReleasesClient({
releases,
}: {
releases: Release[];
}) {
const [activeRepo, setActiveRepo] =
useState<"Mobile App" | "Vue Simulator">("Mobile App");

const filtered = releases.filter(
(r) => r.repo === activeRepo
);

return (
<div className="mt-6 space-y-6">
{/* Tabs */}
<div className="flex gap-6 border-b pb-2">
<button
onClick={() => setActiveRepo("Mobile App")}
className={
activeRepo === "Mobile App"
? "font-semibold text-[#50B78B]"
: "text-zinc-500"
}
>
Mobile App
</button>

<button
onClick={() => setActiveRepo("Vue Simulator")}
className={
activeRepo === "Vue Simulator"
? "font-semibold text-[#50B78B]"
: "text-zinc-500"
}
>
Vue Simulator
</button>
</div>

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 | 🟠 Major

Missing ARIA attributes for accessible tab navigation.

The tab buttons lack proper ARIA attributes needed for screen reader users and assistive technology. Tab interfaces should implement the ARIA tabs pattern with:

  • role="tablist" on the container
  • role="tab" on each button
  • aria-selected="true|false" to indicate the active tab
  • aria-controls linking each tab to its panel
  • Keyboard navigation (Left/Right arrow keys to navigate between tabs)
♿ Proposed accessibility improvements
      {/* Tabs */}
-     <div className="flex gap-6 border-b pb-2">
+     <div role="tablist" className="flex gap-6 border-b pb-2">
        <button
+         role="tab"
+         aria-selected={activeRepo === "Mobile App"}
+         aria-controls="releases-panel"
          onClick={() => setActiveRepo("Mobile App")}
          className={
            activeRepo === "Mobile App"
              ? "font-semibold text-[#50B78B]"
              : "text-zinc-500"
          }
        >
          Mobile App
        </button>

        <button
+         role="tab"
+         aria-selected={activeRepo === "Vue Simulator"}
+         aria-controls="releases-panel"
          onClick={() => setActiveRepo("Vue Simulator")}
          className={
            activeRepo === "Vue Simulator"
              ? "font-semibold text-[#50B78B]"
              : "text-zinc-500"
          }
        >
          Vue Simulator
        </button>
      </div>

      {/* Content */}
-     {filtered.length === 0 ? (
+     <div id="releases-panel" role="tabpanel">
+       {filtered.length === 0 ? (
          <p className="text-sm text-zinc-500">
            No releases yet for {activeRepo}.
          </p>
        ) : (
          filtered.map((release) => (
            <ReleaseCard
              key={`${release.repoSlug}-${release.version}`}
              release={release}
            />
          ))
        )}
+     </div>

For full keyboard support, also add arrow key handlers to move focus between tabs.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @components/Releases/ReleasesClient.tsx around lines 22 - 44, The tab
container div and the two buttons need ARIA roles and keyboard navigation: add
role="tablist" to the div that wraps the buttons, give each button role="tab",
set aria-selected based on the activeRepo state (e.g., aria-selected={activeRepo
=== "Mobile App"}), add aria-controls values that reference the corresponding
panel IDs (create matching id attributes on the release panels), and ensure each
button has an id used by the panel's aria-labelledby; also implement Left/Right
arrow key handlers on the buttons (or a shared keyboard handler) to move focus
between tabs and call setActiveRepo to update the active tab accordingly.


{/* Content */}
{filtered.length === 0 ? (
<p className="text-sm text-zinc-500">
No releases yet for {activeRepo}.
</p>
) : (
filtered.map((release) => (
<ReleaseCard
key={`${release.repoSlug}-${release.version}`}
release={release}
/>
))
)}
</div>
);
}