Adding Tab Navigation to a Long List: OpenSpec Layout Refactor (Part 4)
In this fourth OpenSpec case study, the author refactors the home page layout by introducing a category‑based tab navigation skeleton that replaces a scrolling long list, detailing the exploration of four design options, the chosen implementation using React, TypeScript and Tailwind, and the full TDD‑driven development, testing, verification and archiving workflow.
Problem
Home page displayed all tools in a single long list. With 5 tools and 4 categories it was acceptable, but scaling to 15 tools and 8 categories would require excessive scrolling because there was no category navigation, anchor links, or filtering.
Explore navigation solutions
Used OpenSpec /opsx:explore to generate four candidate designs (sticky top Tab, left mini navigation/drawer, anchor‑jump, sticky category title). The team selected the sticky top Tab navigation (Option A) because the current four categories fit comfortably and a sidebar would be overkill.
Design decisions
State managed with useState<string>('全部') to track the active tab; URL parameters omitted.
Tab click triggers window.scrollTo({ top: 0, behavior: 'smooth' }) to reset scroll.
Active tab highlighted with border-b-2 border‑accent‑500 and accent text color.
No changes to global TopNav, routing, or the static catalog.ts data source.
Implementation
Modified src/app/views/Home.tsx to add a Tab bar, filtering logic, and extracted a ToolCard component. Updated src/app/views/Home.test.tsx with TDD tests for Tab rendering and filtering.
export default function Home() {
const tools = getTools();
const categories = [...new Set(tools.map(t => t.category))].sort();
const [activeTab, setActiveTab] = useState<string>('全部');
const handleTabClick = (tab: string) => {
setActiveTab(tab);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const filteredTools = activeTab === '全部' ? tools : tools.filter(t => t.category === activeTab);
return (
<div className="space-y-12 px-6 py-8">
<div className="text-center py-12">
<h1 className="text-3xl font-bold mb-3">shuge AI Toolbox</h1>
<p>探索 AI 工具,提升工作效率</p>
</div>
{/* Tab navigation */}
<nav className="flex gap-2 border-b border-neutral-200">
<button onClick={() => handleTabClick('全部')
className={`px-4 py-2 text-sm font-medium transition-colors ${activeTab === '全部' ? 'border-b-2 border-accent-500 text-accent-600' : 'text-neutral-500 hover:text-neutral-900'}`}>全部</button>
{categories.map(cat => (
<button key={cat} onClick={() => handleTabClick(cat)}
className={`px-4 py-2 text-sm font-medium transition-colors ${activeTab === cat ? 'border-b-2 border-accent-500 text-accent-600' : 'text-neutral-500 hover:text-neutral-900'}`}>{cat}</button>
))}
</nav>
{/* Content rendering */}
{activeTab === '全部' ? (
categories.map(category => {
const categoryTools = filteredTools.filter(t => t.category === category).sort((a, b) => {
const diff = stagePriority[a.stage] - stagePriority[b.stage];
return diff !== 0 ? diff : a.name.localeCompare(b.name);
});
return (
<section key={category}>
<h2 className="text-xl font-semibold mb-4">{category}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{categoryTools.map(tool => (<ToolCard key={tool.id} tool={tool} />))}
</div>
</section>
);
})
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredTools.sort((a, b) => {
const diff = stagePriority[a.stage] - stagePriority[b.stage];
return diff !== 0 ? diff : a.name.localeCompare(b.name);
}).map(tool => (<ToolCard key={tool.id} tool={tool} />))}
</div>
)}
</div>
);
}
function ToolCard({ tool }: { tool: ReturnType<typeof getTools>[number] }) {
return (
<Link to={`/tools/${tool.id}`} className={`block p-5 rounded-xl border transition-all ${tool.stage === 'planned' ? 'opacity-60' : 'hover:shadow-md'}`} style={{ backgroundColor: 'var(--color-neutral-50)', borderColor: 'var(--color-neutral-200)' }}>
<div className="flex items-start justify-between mb-2">
<h3 className="font-medium">{tool.name}</h3>
{tool.stage === 'planned' && (
<span className="text-xs px-2 py-0.5 rounded" style={{ backgroundColor: 'var(--color-neutral-200)', color: 'var(--color-neutral-600)' }}>Planned</span>
)}
{tool.stage === 'beta' && (
<span className="text-xs px-2 py-0.5 rounded" style={{ backgroundColor: 'var(--color-accent-100)', color: 'var(--color-accent-600)' }}>Beta</span>
)}
</div>
<p className="text-sm">{tool.description}</p>
</Link>
);
}Testing (TDD)
Three‑stage TDD cycle:
Write failing tests for Tab rendering and filtering.
Implement Tab UI until tests pass.
Extract ToolCard component and achieve full test coverage.
Test results after /opsx:apply:
Test Files 6 passed (6)
Tests 30 passed (30)
All tasks complete! You can archive this change with `/opsx:archive`.Verification
Running /opsx:verify produced a report confirming completeness, correctness, and coherence:
## Verification Report: layout-restructure
### Summary
| Dimension | Status |
|--------------|------------------------------|
| Completeness | ✅ All 12 tasks committed |
| Correctness | ✅ All 7 spec scenarios covered |
| Coherence | ✅ Design decisions followed |Manual browser check confirmed that the Tab bar appears correctly, filters as expected, and the page builds without errors:
vite v8.0.12 building client environment for production...
✓ 31 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.46 kB │ gzip: 0.30 kB
dist/assets/index-HNIv7IuC.css 17.34 kB │ gzip: 4.22 kB
dist/assets/index-D0MO6XrT.js 290.38 kB │ gzip: 92.34 kB
✓ built in 105msArchiving
After verification the change was archived with /opsx:archive, moving artifacts to openspec/changes/archive/2026-05-16-layout-restructure/. The apply step generated three commits following the RED‑GREEN‑REFACTOR cycle: 132cb45 – test: add Tab navigation tests to Home de953b9 – feat: add Tab navigation to Home page 49f533a – feat: complete Tab navigation for Home page
These commits were pushed to the GitHub repository https://github.com/shuge-x/shuge-ai-toolbox.
Key artifacts
Proposal, design, specs, review, and tasks files under openspec/changes/layout-restructure/ documenting the rationale, behavior scenarios, and test coverage.
Core decision: use useState for tab state, filter display, and smooth scroll; no URL changes; Tab bar styled with underline highlight.
Lessons learned
Even a tiny UI change benefits from a full OpenSpec workflow, providing traceable decisions and reproducible artifacts.
The Tab solution fits the current four‑category scenario but may need to be replaced by a sidebar or tree view when categories exceed six or multi‑level hierarchies appear.
Keeping filter state out of the URL simplifies the UI; adding useSearchParams later would enable deep‑linking without major refactoring.
Manual post‑apply checks are essential because verification does not compare file diffs; they catch cases where AI reports success without actually modifying code.
Future work
Next iteration will focus on visual polish: adding icons, interaction animations, and refined color details.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shuge Unlimited
Formerly "Ops with Skill", now officially upgraded. Fully dedicated to AI, we share both the why (fundamental insights) and the how (practical implementation). From technical operations to breakthrough thinking, we help you understand AI's transformation and master the core abilities needed to shape the future. ShugeX: boundless exploration, skillful execution.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
