-
Confirm the page is route-worthy. Re-read the user request. If it describes a modal, drawer, dialog, tab inside an existing page, or a settings sub-panel, STOP — this skill does not apply. Otherwise note the route path (e.g. /settings) and the PascalCase component name (e.g. Settings). Verify the path is not already declared in src/renderer/App.tsx before proceeding.
-
Read the reference pages to lock in the exact patterns used in this codebase. Open src/renderer/pages/Home.tsx and src/renderer/pages/Library.tsx and note:
- The import order: React hooks first, then
react-router-dom, then ../utils/api (or wherever the api wrapper lives), then components, then types, then styles if any.
- The state shape (
useState<Item[]>([]), useState(true) for loading, useState<string | null>(null) for error).
- The async
load() declared inside useEffect (NOT useEffect(async () => ...)).
Verify both files use the same pattern before treating it as the convention.
-
Create the page file at src/renderer/pages/{Name}.tsx. Use this exact skeleton, adapting only the type, api call, and rendered markup:
import { useEffect, useState } from 'react'
import { api } from '../utils/api'
import type { Whatever } from '../types'
export default function {Name}() {
const [items, setItems] = useState<Whatever[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const load = async () => {
try {
const data = await api.getWhatever()
setItems(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load')
} finally {
setLoading(false)
}
}
load()
}, [])
if (loading) return <div className="loading-spinner" />
if (error) return <div className="error">{error}</div>
return (
<div className="{name}-page">
{/* page content */}
</div>
)
}
If the actual api function does not yet exist on ../utils/api, add it there following the existing axios wrapper style — do not call axios inline in the page. Verify the file compiles (npx tsc --noEmit) before continuing.
-
Register the route in src/renderer/App.tsx. Add the import alongside the other page imports (alphabetical or grouped by feature — match the file's existing convention). Add a <Route path="/{kebab-name}" element={<{Name} />} /> inside the existing <Routes> block, placed in the same order the navigation lists them. Do NOT wrap it in a new <BrowserRouter> / <HashRouter> — the app already provides one. Verify with grep -n '<Route' src/renderer/App.tsx that your new route appears exactly once.
-
Wire up navigation when navigable. If the page is reachable from the sidebar, open src/renderer/components/Sidebar.tsx (or whatever sibling of App.tsx renders the top-level nav — check src/renderer/components/ if Sidebar.tsx does not exist). Add a <NavLink to="/{kebab-name}">{Label}</NavLink> following the exact JSX shape of the existing entries (same className, same active handling). If the page is only reachable programmatically (e.g. detail page reached from a card click), skip the sidebar and use useNavigate() from the linking page instead.
-
Type the api response. Add or extend the relevant type in src/renderer/types/ so useState<T[]> is fully typed. Do not use any. Do not duplicate types across files — import from the existing types module.
-
Run validation gates before declaring done:
npx tsc --noEmit — must report 0 errors.
npm run lint — must pass (ESLint is configured).
npm test — Vitest suite must still pass. If you added a new api function, add a matching test in tests/unit/api.test.ts mirroring the existing test shape.
- Start the dev app (
npm run dev) and manually navigate to the new route. Confirm the loading spinner appears, then either data or the error branch renders. If you cannot exercise the UI, say so explicitly — do not claim the page works.