From prototyping-skills
This skill generates or scaffolds a Next.js dashboard UI using shadcn/ui with Server Components and Server Actions. It should be used when the user asks to build a UI, dashboard, frontend, visualisation, page, or component, or when working inside a packages/ui directory. It also applies when the user mentions Next.js, shadcn, React Server Components, Server Actions, "make this visible", "add a view for this", or "build the frontend."
npx claudepluginhub kjgarza/marketplace-claude --plugin prototyping-skillsThis skill is limited to using the following tools:
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Generate a Next.js App Router application using shadcn/ui components, React Server Components by default, and Server Actions for mutations. The runtime is Bun. This is always a dashboard-style interface.
These are team defaults. Multiple people work on these prototypes. In execution mode, follow these unless the project CLAUDE.md overrides them.
In Plan mode, suggest alternatives using the deviation protocol from the
prototyping-skills:team-conventions skill: state the default, name the alternative,
explain the trade-off, flag the blast radius, let the human decide.
Follow these three interactive checkpoints on every scaffold:
Auth gate — Before writing any files, use AskUserQuestion to ask:
"Do you want Google OAuth authentication on this UI? (Requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, NEXTAUTH_SECRET env vars)" If yes, scaffold the full auth layer (see Authentication section) before pages.
UI verification — After scaffolding pages, use TaskCreate to queue a Puppeteer screenshot task so the user can visually confirm the app renders correctly.
See the Verification Checklist section at the end for the exact TaskCreate pattern.
lucide-react). Not Heroicons, FontAwesome, etc.useEffect + fetch, not Tanstack Query, not SWR.app/ directory). Not Pages Router, not getServerSideProps."use client" when genuinely needed (event handlers, useState, browser APIs).@repo/types. Never duplicate./about page (see below).Gate this section with AskUserQuestion before scaffolding any files:
"Do you want Google OAuth authentication on this UI? (Requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, NEXTAUTH_SECRET env vars)"
If yes, scaffold the following files using the NextAuth.js v4 pattern. See references/auth.md for full code.
Files to create:
packages/ui/auth.ts — NextAuth config with GoogleProvider + domain allowlist callbackpackages/ui/middleware.ts — Route protection with public path exceptions (/login, /api/auth/**)packages/ui/providers/auth-provider.tsx — SessionProvider wrapper (Client Component)packages/ui/app/(public)/login/page.tsx — Login page with sign-in buttonpackages/ui/app/api/auth/[...nextauth]/route.ts — NextAuth API route handlerRequired env vars (add to .env.local):
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000
Install dependency:
bun add next-auth
Wrap app/layout.tsx body with <AuthProvider>. Protect all routes except /login and /api/auth/** in middleware.
See references/auth.md for complete file contents including multi-domain allowlist pattern.
packages/ui/
├── src/
│ └── app/
│ ├── layout.tsx # Root layout with sidebar/nav
│ ├── page.tsx # Dashboard home
│ ├── [feature]/
│ │ └── page.tsx # Feature pages
│ ├── actions/ # Server Actions
│ │ └── [resource].ts
│ └── components/ # App-specific components
│ ├── ui/ # shadcn/ui components (managed by CLI)
│ └── [feature]/ # Feature-specific components
├── components.json # shadcn/ui config
├── tailwind.config.ts
├── next.config.ts
├── package.json
└── tsconfig.json
package.json must include:
{
"scripts": {
"dev": "next dev --turbopack --port 3000",
"build": "next build"
},
"dependencies": {
"next": "^15",
"react": "^19",
"react-dom": "^19",
"@repo/types": "workspace:*",
"tailwindcss": "^3",
"class-variance-authority": "latest",
"clsx": "latest",
"tailwind-merge": "latest",
"lucide-react": "latest"
}
}
Data loading happens directly in async Server Components. No hooks, no client fetching:
// app/items/page.tsx — Server Component by default, no "use client" needed
import { ItemsTable } from "@/components/items/items-table";
export default async function ItemsPage() {
const res = await fetch("http://localhost:3001/api/items", {
cache: "no-store",
});
const items = await res.json();
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Items</h1>
<ItemsTable items={items} />
</div>
);
}
All mutations use Server Actions defined in app/actions/:
// app/actions/items.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createItem(formData: FormData) {
const name = formData.get("name") as string;
await fetch("http://localhost:3001/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
revalidatePath("/items");
}
export async function deleteItem(id: string) {
await fetch(`http://localhost:3001/api/items/${id}`, { method: "DELETE" });
revalidatePath("/items");
}
Using Server Actions in components:
// components/items/create-item-form.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { createItem } from "@/app/actions/items";
export function CreateItemForm() {
return (
<form action={createItem} className="flex gap-2">
<Input name="name" placeholder="Item name" required />
<Button type="submit">Create</Button>
</form>
);
}
Preferred: Install the full shadcn sidebar block:
npx shadcn@latest add sidebar-01
This installs the complete sidebar-01 block from ui.shadcn.com/blocks/sidebar, including all sub-components, styles, and layout wrapper. Customize the nav items after installation.
Fallback (if block install is not appropriate): Use the manual pattern below.
// app/layout.tsx
import { SidebarProvider, Sidebar, SidebarContent, SidebarMenu,
SidebarMenuItem, SidebarMenuButton } from "@/components/ui/sidebar";
import { LayoutDashboard, Settings, Info } from "lucide-react";
import Link from "next/link";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SidebarProvider>
<div className="flex min-h-screen">
<Sidebar>
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/"><LayoutDashboard className="mr-2 h-4 w-4" />Dashboard</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/settings"><Settings className="mr-2 h-4 w-4" />Settings</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/about"><Info className="mr-2 h-4 w-4" />About</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
</Sidebar>
<main className="flex-1 p-6">{children}</main>
</div>
</SidebarProvider>
</body>
</html>
);
}
Every app MUST include an /about route. Scaffold it during initial setup:
// app/about/page.tsx — Server Component
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Info } from "lucide-react";
export default function AboutPage() {
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Info className="h-6 w-6" /> About
</h1>
<Card>
<CardHeader>
<CardTitle>[Project Name]</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-muted-foreground">
<p>[Brief description of what this prototype does]</p>
<p className="text-sm">Built with the prototyping-skills stack.</p>
</CardContent>
</Card>
</div>
);
}
Add the About link to the sidebar layout alongside Dashboard and Settings.
Use shadcn/ui's CSS custom properties (design tokens) to separate content from style. Never hard-code colors, spacing, or font sizes.
Do:
<p className="text-muted-foreground">Subtle text</p>
<div className="bg-card border-border rounded-lg p-4">Card</div>
<span className="text-destructive">Error</span>
Don't:
<p className="text-gray-500">Subtle text</p>
<div className="bg-white border-gray-200 rounded-lg p-4">Card</div>
<span className="text-red-600">Error</span>
Token reference (use these, not raw Tailwind colors):
bg-background, bg-card, bg-popover, bg-mutedtext-foreground, text-muted-foreground, text-card-foregroundborder-border, border-inputbg-primary, bg-secondary, bg-accent, bg-destructivep-4, gap-6, space-y-4) — don't invent custom spacingbunx shadcn@latest add [component].<Table> or @tanstack/react-table with shadcn styling when sorting/filtering is needed.recharts with shadcn chart wrapper (ChartContainer, ChartTooltip) when data visualisation is needed. See references/charts.md for BarChart, LineChart, and PieChart patterns.loading.tsx files (Next.js Suspense boundaries), not spinner state variables.error.tsx files per route segment.All scaffolded UIs must meet WCAG 2.1 AA. Apply these rules during generation:
Tab in logical order.aria-label="Delete item") and form inputs without visible labels (aria-label or aria-labelledby).<nav>, <main>, <header>, <aside> for structural regions — never <div> for everything.outline: none without replacing it with a custom visible focus indicator. shadcn uses focus-visible:ring — keep it.<img> elements need descriptive alt text. Decorative images use alt="".aria-expanded, aria-haspopup, keyboard interactions on dropdowns/dialogs. Do not override radix ARIA attributes.Ask: "Does this component need useState, useEffect, event handlers, or browser APIs?"
"use client" at the topCommon pattern: Server Component fetches data, passes it to a Client Component for interactivity.
cn() utility (from lib/utils.ts) for conditional classes.class strategy with shadcn's built-in dark mode support.After scaffolding pages, create this task to visually confirm the app renders correctly:
TaskCreate({
subject: "Verify UI renders with Puppeteer",
description: "1. Start dev server: bun dev (in packages/ui)\n2. Navigate to http://localhost:3000\n3. Take a screenshot and confirm: sidebar visible, main content area renders, no console errors\n4. If auth is enabled: confirm redirect to /login when unauthenticated\n5. Sign in and confirm protected routes load"
})