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.

Shuge Unlimited
Shuge Unlimited
Shuge Unlimited
Adding Tab Navigation to a Long List: OpenSpec Layout Refactor (Part 4)

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 105ms

Archiving

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.

首页改造后的 Tab 导航效果 - 全部 Tab 默认状态
首页改造后的 Tab 导航效果 - 全部 Tab 默认状态
Tab 切换筛选效果 - 开发工具
Tab 切换筛选效果 - 开发工具
切回「全部」恢复效果
切回「全部」恢复效果
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TypeScriptReactFrontend ArchitectureTDDTailwind CSSTab NavigationOpenSpec
Shuge Unlimited
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.