Chakra UI v3 Builder
chakra-ui/chakra-ui
Builds responsive, accessible Chakra UI v3 components and layouts, sets up Chakra in new or existing projects, and designs themes with tokens, semantic tokens and recipes.
Build Next.js dashboard pages that consume domain proxy APIs.
$ npx skills add adam-s/intercept --skill dashboard-builder -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install adam-s/intercept dashboard-builder --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/adam-s/intercept.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/dashboard-builder .claude/skills/dashboard-builder && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "dashboard-builder" agent skill from https://github.com/adam-s/intercept/tree/main/.claude/skills/dashboard-builder into .claude/skills/dashboard-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dashboard-builder", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/adam-s/intercept/tree/main/.claude/skills/dashboard-builderType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add adam-s/intercept --skill dashboard-builder -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install adam-s/intercept dashboard-builder --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/adam-s/intercept.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/dashboard-builder .agents/skills/dashboard-builder && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "dashboard-builder" agent skill from https://github.com/adam-s/intercept/tree/main/.claude/skills/dashboard-builder into .agents/skills/dashboard-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dashboard-builder", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add adam-s/intercept --skill dashboard-builder -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install adam-s/intercept dashboard-builder --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/adam-s/intercept.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/dashboard-builder .cursor/skills/dashboard-builder && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "dashboard-builder" agent skill from https://github.com/adam-s/intercept/tree/main/.claude/skills/dashboard-builder into .cursor/skills/dashboard-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dashboard-builder", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/adam-s/intercept.git --path .claude/skills/dashboard-builder--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add adam-s/intercept --skill dashboard-builder -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install adam-s/intercept dashboard-builder --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/adam-s/intercept.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/dashboard-builder .gemini/skills/dashboard-builder && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "dashboard-builder" agent skill from https://github.com/adam-s/intercept/tree/main/.claude/skills/dashboard-builder into .gemini/skills/dashboard-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dashboard-builder", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install adam-s/intercept dashboard-builderInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add adam-s/intercept --skill dashboard-builder -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/adam-s/intercept.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/dashboard-builder .github/skills/dashboard-builder && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "dashboard-builder" agent skill from https://github.com/adam-s/intercept/tree/main/.claude/skills/dashboard-builder into .github/skills/dashboard-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dashboard-builder", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add adam-s/intercept --skill dashboard-builder -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install adam-s/intercept dashboard-builder --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/adam-s/intercept.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/dashboard-builder .opencode/skills/dashboard-builder && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "dashboard-builder" agent skill from https://github.com/adam-s/intercept/tree/main/.claude/skills/dashboard-builder into .opencode/skills/dashboard-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dashboard-builder", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
dashboard-builderBuild Next.js dashboard pages that consume domain proxy APIs.
Dashboard Builder is an agent skill from adam-s/intercept. Build Next.js dashboard pages that consume domain proxy APIs. Use when the user wants to create a dashboard, build a UI page, add a search interface, display data from captured APIs, create comparison views, or build any frontend that calls /api/<domain/ endpoints.
Its SKILL.md is about 8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Frontend & Design. It works with Next.js. The repository describes itself as: Turn any website into a typed JSON API using self improving agents. The licence is MIT.
4 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 6451b89. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
pnpmcurlnodenpxFrom the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
ui.shadcn.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Dashboard Builder loads about 8k tokens when it runs. Until then it costs about 71 tokens; SKILL.md has 3,527 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from adam-s/intercept at commit 6451b89, republished under its MIT licence (© adam-s). 3,527 words, ~7,952 tokens.
.claude/skills/dashboard-builder/SKILL.md (or your agent's skills folder).Create Next.js dashboard pages that consume domain proxy API endpoints. Each page lives in apps/web/src/app/(dashboard)/ and uses shadcn/ui components.
Development principle: DEBUG logging is mandatory. The build loop IS the debug-log + screenshot loop. import { DEBUG } from '@interceptor/shared' in every new file. Add DEBUG('component-name', () => ({ step, data })) at every data flow point: API fetch, response parsing, state updates, render decisions. Build a component → check debug logs to verify data flow → screenshot it → fix what's wrong → re-screenshot. Verification output is required input for the next step (see CLAUDE.md "The Rule That Makes This Work").
Single browser instance — sequential calls only. See api-discovery skill "Gotchas" section for details and code patterns.
Prompt compliance gate: Before committing: list every prompt requirement, state evidence for each (curl output, screenshot, Patchright click). Any requirement without evidence = not done. Loop until all have evidence.
The best way to produce quality UI is to copy an existing one. When building a dashboard:
Pick a real website as the template. Choose a well-designed site that serves similar data. The gap between your screenshot and the template IS the bug. See /packages/test-server for test site examples and /domains/boardshop/ for reference domain patterns.
Screenshot the template. Capture the target site at 1280x800 and 375x800 (mobile):
node -e "
const { chromium } = require('patchright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto('TARGET_URL', { waitUntil: 'networkidle' });
await page.screenshot({ path: '/tmp/template-desktop.png' });
await page.setViewportSize({ width: 375, height: 800 });
await page.screenshot({ path: '/tmp/template-mobile.png' });
await browser.close();
})();
"Build to match. After each change, screenshot your work AND read the template screenshot. Compare:
The gap between screenshots IS the bug. This is objective — no subjective "does it look good." Either your layout matches the template or it doesn't. Fix the differences.
When the prompt adds features not in the wireframe: Add new features in a way that preserves the wireframe's layout structure. Inline additions (badges on existing rows, tooltips on existing elements) are preferred over new layout sections (sidebars, panels, extra columns). If a feature requires a new layout section, place it BELOW the main content on mobile and as a narrow aside on desktop — never wider than 25% of the viewport.
If the reference site uses a legacy aesthetic (custom fonts, table-based layout, non-card list items), do NOT abandon shadcn/ui entirely. Instead:
style prop for brand colors and typography..custom-search-input { border: 1px solid #ccc; }, stop and use <Input className="..." style={{ ... }} /> instead.// Prefer Tailwind classes when a utility exists
<Input className="w-36 h-6 text-[9pt] font-mono border-gray-300" placeholder="Search..." />
// Use inline style only for values Tailwind can't express (custom brand fonts, exact hex)
<Input
className="w-36 h-6 text-[9pt]"
style={{ fontFamily: 'Verdana, sans-serif' }}
placeholder="Search..."
/>
// For plain-text link buttons (like HN "comments" or "More"):
<Button variant="link" className="p-0 h-auto text-[7pt] no-underline hover:underline"
style={{ color: '#828282', fontFamily: 'Verdana, sans-serif' }}>
{comments} comments
</Button>
// For error banners matching a non-shadcn aesthetic:
<Alert variant="destructive" className="rounded-none"
style={{ background: '#ffe0e0', border: '1px solid #cc0000' }}>
<AlertDescription>{error}</AlertDescription>
</Alert>The rule: if it's clickable, use Button. If it shows a status message, use Alert. Override the visual tokens, not the component choice. A raw <button> or <div> with inline styles loses focus management, keyboard handling, and ARIA attributes that shadcn provides for free.
Every clickable element must have a 44px minimum touch target. This includes small visual elements like upvote arrows, star icons, and close buttons. Wrap small visuals in an accessible button: <Button variant="ghost" size="icon" className="h-6 w-6 min-h-[44px] min-w-[44px]"><span className="text-xs">▲</span></Button>.
Save template screenshots to /tmp/template-<domain>/ for reference throughout the build.
gap-4/gap-6 between sections; p-4 inside cards — no arbitrary pixelstext-2xl font-bold → labels text-sm text-muted-foreground uppercase tracking-wide → values text-2xl font-semiboldtext-muted-foreground = secondary<Skeleton> (loading) + empty state message — never blankborder border-border/50 rounded-lg — no heavy shadowsDomain plugins registered, pnpm run dev (ports 3000/3001).
Verify the data layer returns real data before building UI. For HTTP routes: curl. For WebSocket streams: connect and observe messages. For any protocol: the verification must produce observable output proving real data flows end-to-end. If you can't verify it, you can't build on it.
Type-verify the API response. After curl-verifying an endpoint, compare the curl JSON output field names against your TypeScript response interface. If a field in your interface does not appear in the curl output (or vice versa), fix the type before writing any component code. as ResponseType does not validate at runtime — mismatched fields produce undefined silently.
Routes must use network interception, not DOM extraction. Every route that serves data must intercept a network request (XHR, WebSocket, GraphQL, etc.) — not parse rendered HTML via page.evaluate(). If a route uses page.evaluate() for data extraction, it violates the discovery protocol and must be rewritten. The Transport Elimination table from discovery.md must exist before any route is created.
If any endpoint returns empty or errors, stop and fix the API layer using debug-logs skill. If data looks wrong or encoded, see CLAUDE.md "Unexpected Output Is Information, Not Failure" — investigate the transformation before concluding something is broken.
After API routes are proven with curl, cache ALL responses as fixtures. This eliminates browser dependency during UI development — every reload is instant (0ms vs 30-60s).
mkdir -p data/fixtures/{domain}
curl -s http://localhost:3001/api/{domain}/search?q=test > data/fixtures/{domain}/search.json
curl -s http://localhost:3001/api/{domain}/detail/123 > data/fixtures/{domain}/detail.jsonThen develop with FIXTURE_DIR=data/fixtures pnpm dev — the API serves cached data instantly. Switch to live mode only for final integration testing.
Why this is mandatory: UI iteration requires 10-50 reloads. At 30-60s per live request, that's 5-50 minutes of pure waiting. With fixtures, it's under 1 second total.
Split components by view — one file per view, one shared types file. Each view component should be under 200 lines:
*-types.ts — types, interfaces, helper functions, and shared constants (PAGE_SIZE, API paths, color maps)Run pnpm biome check --write --unsafe . before manual lint cleanup. Only manually fix what auto-fix can't.
mkdir -p apps/web/src/app/\(dashboard\)/<page-name>page.tsx importing a <PageContent /> client componentLayout group placement rule: Pages that match a full-page reference site design (their own header, footer, and nav) should still be placed inside (dashboard)/ and must add a local layout.tsx to opt out of the shared shell. Do NOT place the page outside the (dashboard) group — that removes it from the app's routing conventions and makes it invisible to the sidebar. If the reference site has its own nav/header, implement that nav inside the page component, not at the layout level.
// apps/web/src/app/(dashboard)/<page-name>/layout.tsx — full-viewport overlay to cover parent sidebar
export default function Layout({ children }: { children: React.ReactNode }) {
return <div className="fixed inset-0 z-50 overflow-auto bg-background">{children}</div>;
}The content component must NOT contain fixed inset-0 or z-50. Viewport-level positioning belongs in the layout.tsx. The content component handles data and rendering only.
Create apps/web/src/app/(dashboard)/<page-name>/<page-name>-content.tsx with 'use client'. Use shadcn/ui components — not raw divs. Standard search page pattern:
nuqs hooks from @/lib/url-state for view switching, selected IDs, and search queries. This gives back button, deep linking, and shareable URLs for free:import { useView, useSelectedId, useSearchQuery } from '@/lib/url-state';
const [view, setView] = useView(); // ?view=list|detail|search
const [id, setId] = useSelectedId(); // ?id=12345
const [q, setQ] = useSearchQuery(); // ?q=search+term
// Navigate: setView('detail'); setId(item.id);
// Back to list: setView('list'); setId(null);results, loading, error (view/query/id are URL params, not useState)/api/<domain>/<endpoint>?q=${encodeURIComponent(q)} (relative URL — see CLAUDE.md "Frontend API URLs")flex flex-1 flex-col gap-4 p-6 max-w-4xl mx-auto w-fullInput + Button with onKeyDown Enter handlerSkeleton cards), empty ("No results for..."), idle ("Search above to get started"), populated (result Card list with hover)GATE: Screenshot the component before writing anything else.
DEBUG() to the API route handler, re-fetch, read the log, fixYou cannot add Step 5 (multi-domain composition) on top of a broken Step 4. Each layer must be proven before building the next.
Always sequential, catch per source. If a source returns null, mark offline — never let one failure break the page.
Debug each source independently first. Before composing sources together, curl each one and confirm it returns data. Add DEBUG('fetch-sourceA', () => ({ status, count: data?.length })) in the component's fetch function to see which source is failing at runtime. When sources are composed, a silent failure in one source produces confusing results in the merged view — debug logs tell you exactly which source returned null and why.
When comparing data from two browser-dependent domains (e.g. two platforms showing the same product), the singleton browser navigates to each domain's page in turn. Each navigation clobbers the previous page state. This means:
Frontend must call domains sequentially — never Promise.all. Source A navigates, extracts data, returns. Then source B navigates, extracts, returns. The data from A is safe because it was already extracted and returned as JSON before B's navigation started.
Show progress during sequential fetches — tell the user which source is loading. "Searching <domain-a>... (1 of 2)" then "Searching <domain-b>... (2 of 2)". Without this, the user sees a spinner for 20+ seconds with no indication of progress.
Each source's route must fully extract before returning — don't rely on the browser still being on the same page after the route handler returns. Navigate, wait, extract, return JSON. The next domain route will navigate away.
// CORRECT — sequential, with progress updates
setLoadingMessage(`Searching ${sources[0].name}...`);
const resultA = await fetch(`/api/${sources[0].domain}/search?q=${q}`).then(r => r.json()).catch(() => null);
setLoadingMessage(`Searching ${sources[1].name}...`);
const resultB = await fetch(`/api/${sources[1].domain}/search?q=${q}`).then(r => r.json()).catch(() => null);
// Now merge — both datasets are in memory, browser state doesn't matter
const merged = mergeResults(resultA?.items ?? [], resultB?.items ?? []);Merge by a stable compound key (not free-text titles) to avoid duplicate cards:
function mergeKey(venue: string, date: string): string {
const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
return `${norm(venue)}|${norm(date)}`;
}
const byKey = new Map<string, Record<string, unknown>>();
for (const item of sourceAResults) byKey.set(mergeKey(item.venue, item.date), { ...byKey.get(mergeKey(item.venue, item.date)), sourceA: item });
// repeat for sourceB... each Map entry = one display rowRules: Use stable fields (venue+date for events, company+title+city for jobs, DOI for papers). Normalize aggressively: lowercase, strip punctuation, parse dates to ISO. Normalize labels ("Section 101" = "Sec 101" = "101"). Single-source entities still appear with one badge.
Filter before merging: Validate results belong to the query. Use startsWith or word-boundary regex — not includes. Skip disqualifying keywords.
You have been screenshotting and debugging throughout Steps 4-5. This step is the comprehensive final sweep.
Write a Patchright script that tests every user journey and captures every state:
1. Navigate to the page (first visit, no data) → screenshot → describe what you see
2. Perform a search → screenshot results → describe what you see
3. Click into a detail view → screenshot → describe what you see
4. Click every interactive element (buttons, favorites, filters, downloads) → verify each responds
5. Set viewport to 375x812 → screenshot → describe what you see
6. Check browser console for errors after each interactionFor each screenshot: Read it. Describe what you see in one sentence. If ANYTHING is wrong (broken layout, missing data, dead button, overlapping text, vague error message, content touching edges) — fix it, re-screenshot, confirm the fix.
Iterate until screenshots show zero issues. See visual-dev skill "Stopping Criteria" for the judgment framework.
Only commit after this step produces zero-issue screenshots across all states and viewports.
shadcn/ui — catalog: https://ui.shadcn.com/docs/components. Install upfront:
cd apps/web && npx shadcn@latest add card badge sheet table skeleton alert input button -y| Pattern | When | Implementation |
|---|---|---|
| Inline expand | Detail fits 2-4 lines | Collapsible row or Tooltip |
| Side sheet (default) | Detail needs its own layout | Sheet from right, list stays visible |
| Full page | Detail has sub-nav/tabs/charts | router.push('/item/[id]') |
State: page (0-indexed), PAGE_SIZE = 25, totalPages = Math.ceil(total / PAGE_SIZE). Fetch with ?limit=${PAGE_SIZE}&offset=${page * PAGE_SIZE}. Reset page to 0 on filter change. Optional reusable pager: apps/web/src/components/ui/grid-pagination.tsx with ChevronsLeft/Right + ChevronLeft/Right buttons from lucide-react.
| State | What to show |
|---|---|
| Idle | Icon in rounded container (w-16 h-16 rounded-2xl bg-muted/50) + heading (text-base font-medium) + description (text-sm text-muted-foreground text-center) + suggestion chips (rounded-full bg-muted) |
| Loading | Skeleton matching real content shape |
| Empty | Search icon (w-10 h-10 mx-auto text-muted-foreground/40) + message + sub-message with suggestions ("Try different keywords or check spelling") |
| Populated | Real content with typography hierarchy |
| Detail loading | Skeleton rows inside Sheet |
| Detail populated | Full detail with visual hierarchy. External links as outline buttons with external-link icon: <Button variant="outline" size="sm" className="gap-1.5" asChild><a href={url} target="_blank">...</a></Button> |
| Partial offline | Alert naming the failing source |
| Full offline | Alert per source with recovery instruction |
| Error | Card with border-destructive/50 and user-facing error message. Silent catch {} blocks must at minimum set an error state. |
| Pattern | Component | Notes |
|---|---|---|
| List items | Card + CardContent | Never raw <div>. Hover state + border. |
| Source labels | Badge | Consistent color across all views |
| Detail view | Sheet from right | List stays visible behind |
| Comparison grid | Table | Rows = entities, cols = sources. Best value in green. Missing = — |
| Loading | Skeleton | Match real content dimensions |
| Errors | Alert + AlertDescription | Name the specific source, never generic |
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 min-w-0">{/* main content */}</div>
<div className="w-full lg:w-72 flex-shrink-0"><Card>{/* sidebar */}</Card></div>
</div>Semantic tokens only: bg-background, bg-muted, text-foreground, text-muted-foreground, hover:bg-muted. Accent: bg-blue-950/30 border-blue-500/20 (not bg-blue-50).
| Pattern | Implementation |
|---|---|
| Search bar icon | Search magnifying glass icon (absolute left-3, w-4 h-4) with pl-9 on the input for instant visual recognition |
| Back button arrow | Every "Back" button needs a left arrow icon (ArrowLeft w-4 h-4) prepended for navigation affordance |
| Responsive padding | All content wrappers: p-4 sm:p-6 (not just p-6). Mobile needs tighter padding. |
| Source count badges | When showing counts from multiple sources, use colored Badge components instead of plain text. Hide sources with 0 results. |
| Mobile action buttons | Buttons with text labels that don't fit on mobile should collapse to icon-only (size="icon") with className="sm:hidden" / className="hidden sm:flex" |
For user state (favorites, tracking, bookmarks) with no database requirement: dedicated domain plugin with module-level Set/Map + browserRequired: false routes. Resets on server restart. Register in apps/api/src/register-domains.ts.
Optimistic UI: Update React state immediately, fire API call in background. Don't await server sync before updating UI. .catch(() => {}) — don't revert on network error.
Same pattern as Multi-Source Entity Merging above, but with crossListed tracking. Build Map<key, { sources[], crossListed }>. Normalize: lowercase, strip legal suffixes (Inc, LLC, Corp), strip punctuation.
Cross-listed UI: Show "N sites" badge on crossListed: true cards. In detail Sheet, show per-source price/salary comparison side by side. If prices differ: "Source B lists $X higher".
Row of Button variants (default for active, outline for inactive), size="sm", optimistic updates. Place below entity details in Sheet.
Flatten + sort by date descending: sources.flat().sort((a, b) => b.date.localeCompare(a.date)). Each item: { date, type, title, subtitle, source, link }. Visual: vertical line with colored dots per source. Tab bar for merged vs per-source views.
For long-running operations: POST to start (returns jobId), store in Map<jobId, Job>, useEffect with setInterval(1000) polling active jobs. Auto-stop when all complete/error. Show <Progress value={job.progress} /> + status <Badge>.
Inline SVG <polyline>, map data points to coords, green (#22c55e) if up, red (#ef4444) if down. Filter nulls, compute min/max/range, scale to 120x32 viewbox. For complex charts: @visx/shape + @visx/scale.
For brand-specific palettes, use a COLORS constants object with inline style (not Tailwind). Use className for layout (flex, gap, padding) and style for colors. Touch targets: min-w-[44px] min-h-[44px]. Bottom nav bar for mobile app feel.
Recursive component with depth prop. Colored left-border per depth: borderColor: hsl(${depth * 60 % 360}, 50%, 40%). Indent: marginLeft: Math.min(depth, 6) * 16. Click header to collapse/expand. Cap at maxDepth 6.
function CommentTree({ comment, depth = 0 }: { comment: Comment; depth?: number }) {
const [collapsed, setCollapsed] = useState(false);
return (
<div style={{ marginLeft: Math.min(depth, 6) * 16 }}>
<div className="py-2 border-l-2 pl-3" style={{ borderColor: `hsl(${depth * 60 % 360}, 50%, 40%)` }}>
<button onClick={() => setCollapsed(!collapsed)}>{/* author, score, timeAgo */}</button>
{!collapsed && <>
<p>{comment.body}</p>
{comment.replies.map(r => <CommentTree key={r.id} comment={r} depth={depth + 1} />)}
</>}
</div>
</div>
);
}Wrap <iframe> in aspect-video container. Prefer -nocookie embed domains. For local video: <video controls> with server-side Content-Range support.
Return raw new Response() (not c.json()) with Content-Range headers. Prevent path traversal (.. and / in filename). Import createReadStream from node:fs, Readable from node:stream. Status 206 for partial content.
const range = c.req.header('range');
if (range) {
const [startStr, endStr] = range.replace('bytes=', '').split('-');
const start = parseInt(startStr), end = endStr ? parseInt(endStr) : stat.size - 1;
return new Response(Readable.toWeb(createReadStream(filepath, { start, end })) as ReadableStream, {
status: 206, headers: { 'Content-Range': `bytes ${start}-${end}/${stat.size}`, 'Content-Type': 'video/mp4' },
});
}Dashboard components use relative URLs: /api/<domain>/<path> (see CLAUDE.md "Frontend API URLs"). Browser must be connected or proxy returns 503. POST endpoints need Content-Type: application/json.
| Problem | Fix |
|---|---|
| CORS error | Server has CORS enabled — check port |
| 503 from proxy | Connect browser at /browser?profile=<domain> |
| Empty results | curl the endpoint directly first |
| Unbounded list/table | Wrap in max-h-[300px] overflow-y-auto (lists) or max-h-[600px] overflow-y-auto (tables) — never let content grow unbounded inside a card |
| JSX comment in ternary | {/* comment */} inside ? : breaks the parser — put comments outside the ternary or wrap in <> fragment |
| Hydration error | Add 'use client' at top |
| Page not found | Directory name must match URL path |
| Fetch timeout on browser-dependent routes | browserFetch default is 20s — use AbortSignal.timeout(45000) on the dashboard fetch wrapper |
These principles override the implementation patterns in this skill file. They do not override CLAUDE.md workflow rules or verification gates.
search → click result → scroll → click download → verifycatch {} = user stares at a spinner forever. Every catch must surface a message.cursor-pointer, color, underline, or icon must be visible by default.setState(x) then call a function that reads state — it sees the old value. Pass x as a parameter. fetchResults(suggestion) not setQuery(s); handleSearch()p-4 minimum for cards, p-6 for sheets. If text touches the border, spacing is broken."Kendrick Lamar" fails. "Kendrick Lamar · Jun 15 · Allegiant Stadium · from $89" passes."Searching Source B... (2 of 3)" not a generic spinner for 30 seconds."Austin, TX" and "Austin TX" must match. Trim, lowercase, strip punctuation.<button className="min-h-[44px] min-w-[44px] flex items-center"><span className="text-xs">text</span></button>. The rendered text stays small; the tap target is accessible.dangerouslySetInnerHTML requires a sanitization decision. If an API returns HTML fragments, make one of these choices: (a) sanitize in the API route handler using sanitize-html before sending to the client, (b) sanitize in the component using DOMPurify.sanitize(html) before passing to dangerouslySetInnerHTML, or (c) document specifically why the source is trusted and what prevents injection. A biome-ignore comment alone is not sufficient documentation.{ data: T, loading: boolean, error: string | null }. One source failing must not block or lose the other's results.Skeleton placeholders in the result container, (3) status text showing which source is active.DEBUG() and returns without calling setError() or toast.error() is a silent failure. Rule: if a catch block does not call setError, toast.error, or setItemError, it must have a comment explaining why the failure is intentional and non-user-impacting.After building a dashboard, verify it works with at least 3 different inputs that exercise different data shapes:
If the dashboard only works for the first input you tested, it's not done.
Verify utility functions with boundary values. Time-relative functions (timeAgo, formatDate, relativeTime) must be tested with edge cases before shipping: a timestamp 30 seconds ago, 90 seconds ago, 90 minutes ago, 25 hours ago, 8 days ago. Test inline: console.log(timeAgo(Date.now()/1000 - 30)) — confirm it returns "30 seconds ago", not "0 minutes ago".
Use visual-dev skill + debug-logs skill. Every page must pass before committing.
© adam-s, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .claude/skills/dashboard-builder of adam-s/intercept.
Open the folder on GitHubat commit 6451b89
Dashboard Builder next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Dashboard Builder this skilladam-s/intercept | 189 | — | ~8k | Automated safety check: Pass | MIT | |
| Chakra UI v3 Builderchakra-ui/chakra-ui | 41k | — | ~3.1k | Automated safety check: Pass | MIT | |
| React View Transitions Guidevercel-labs/next-beats | 167 | 10 repos | ~3.1k | Automated safety check: Pass | MIT | |
| Website ClonerJCodesMore/ai-website-cloner-template | 36k | — | ~1.7k | Automated safety check: Pass | MIT | |
| Wcag Audit PatternsvmDeshpande/ai-agent-automation | 178 | 11 repos | ~610 | Automated safety check: Pass | Apache-2.0 | |
| Vercel React Best Practicessanity-io/sanity | 6.4k | 130 repos | ~1.6k | Automated safety check: Pass | MIT |
chakra-ui/chakra-ui
Builds responsive, accessible Chakra UI v3 components and layouts, sets up Chakra in new or existing projects, and designs themes with tokens, semantic tokens and recipes.
vercel-labs/next-beats
Guides implementing animated page and state transitions in React with the native View Transition API, covering shared elements, list reorders and directional navigation.
JCodesMore/ai-website-cloner-template
Rebuilds existing web pages as editable local code that matches their content, assets, responsive layout and interactions, including Framer sites and animated pages.
vmDeshpande/ai-agent-automation
Conduct WCAG 2.2 accessibility audits with automated testing, manual verification, and remediation guidance.
sanity-io/sanity
React and Next.js performance optimization guidelines from Vercel Engineering.
cloudflare/skills
Set up, repair, or migrate to Cloudflare Turnstile bot verification in an existing frontend and backend, including server-side Siteverify.
adam-s/intercept
Discover any website's API and create domain plugins with proxy routes.
adam-s/intercept
Build a complete application from a short description. An agent skill from adam-s/intercept.
adam-s/intercept
Iterative debugging with targeted logs. An agent skill from adam-s/intercept.
adam-s/intercept
Use sub-agents to iteratively improve dashboard-building instructions.
adam-s/intercept
Use sub-agents as test subjects to iteratively improve .claude/ instruction files.
adam-s/intercept
Bottom-up systematic validation for multi-layer architectures.
Works with
Categories
Build Next.js dashboard pages that consume domain proxy APIs. Dashboard Builder is an agent skill from adam-s/intercept.js dashboard pages that consume domain proxy APIs.
Dashboard Builder fits situations like: the user wants to create a dashboard; build a UI page; add a search interface; display data from captured APIs.
Run `npx skills add adam-s/intercept --skill dashboard-builder -a claude-code`. Or copy the skill folder (.claude/skills/dashboard-builder in adam-s/intercept) into .claude/skills/dashboard-builder in your project. Claude Code loads it when a task matches its description.
Run `npx skills add adam-s/intercept --skill dashboard-builder -a codex`. Or copy the skill folder (.claude/skills/dashboard-builder in adam-s/intercept) into .agents/skills/dashboard-builder in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add adam-s/intercept --skill dashboard-builder -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/dashboard-builder, .gemini/skills/dashboard-builder, .github/skills/dashboard-builder and .opencode/skills/dashboard-builder in your project.
Going by SKILL.md and its folder, Dashboard Builder needs the command-line tools its instructions call (pnpm, curl, node and npx).
SKILL.md names 1 domain. As links in the text: ui.shadcn.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Dashboard Builder is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 8k tokens (SKILL.md is roughly 32k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Dashboard Builder: Chakra UI v3 Builder (chakra-ui/chakra-ui, 41k stars), React View Transitions Guide (vercel-labs/next-beats, 167 stars), Website Cloner (JCodesMore/ai-website-cloner-template, 36k stars) and Wcag Audit Patterns (vmDeshpande/ai-agent-automation, 178 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
adam-s (a GitHub user) maintains it in adam-s/intercept, which has 189 GitHub stars. The repository holds 10 skills in this directory. The repository was last updated on July 31, 2026.
Source: adam-s/intercept on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.