一键导入
frontend
Use when building UI components, pages, layouts, dashboards, forms, or 3D scenes. Also for design systems, responsive design, and server components.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when building UI components, pages, layouts, dashboards, forms, or 3D scenes. Also for design systems, responsive design, and server components.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when designing functions, modeling data, choosing types, drawing module boundaries, or deciding what depends on what. Use when evaluating architecture, extracting abstractions, or shaping vertical slices.
Use when writing, reviewing, or refactoring code in any language. Use for architecture decisions, system design, component boundaries, and code quality judgment. Always relevant when touching source code.
Use when writing or reviewing comments, docstrings, names, control flow, or file organization. Use when evaluating readability, choosing identifiers, splitting files, or applying naming conventions. Use when removing AI tells (slop) from code prose — comments, docs, error messages, commit messages, PR descriptions. Covers the visible surface of code.
Run kirby's review engine on a PR or a commit, OUTSIDE the orchestrator loop. Local output only — findings are presented in-conversation, nothing is posted, no Provider config needed.
Launch kirby-bot orchestrator in background, relay phase transitions live from run.jsonl.
Extract and organize existing code into reusable modules, functions, and components with thoughtful APIs.
| name | frontend |
| description | Use when building UI components, pages, layouts, dashboards, forms, or 3D scenes. Also for design systems, responsive design, and server components. |
Design work produces generic output without project context. Before any UI implementation, you MUST have answers to these blocking questions:
Where to find answers (in order):
.impeccable.md at project root — if it has the context, proceedBefore shipping any UI, run this checklist. If 3+ items are true, the design looks AI-generated:
The test: If someone saw this UI and said "AI made this", would they be right? A distinctive interface makes someone ask "how was this made?" not "which AI made this?"
"use client" only for hooks/event handlers/browser APIs — over-marking kills SSR. Never blanket-mark a whole page; push the directive down to the smallest interactive leaf and keep the page/shell a server componentuseEffect for derived state = anti-pattern. Compute inline during render, never sync with effectskey={index} dynamic lists = silent bugs on reorder/delete. Stable IDs onlyfetch() Server Components caches by default prod. { cache: 'no-store' } or revalidateFile colocation — keep everything related to a component together:
src/components/
TaskList/
TaskList.tsx # Component implementation
TaskList.test.tsx # Tests
TaskList.stories.tsx # Storybook stories (if using)
use-task-list.ts # Custom hook (if complex state)
types.ts # Component-specific types
Composition over configuration — props create exponential state space, children are linear:
// Good: Composable — each piece is independent
<Card>
<CardHeader>
<CardTitle>Tasks</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// Bad: Over-configured — title+headerVariant+bodyPadding = 8 combinations to test
<Card
title="Tasks"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>
Separate data fetching from presentation — container handles data/states, presentational component is pure rendering:
// Container: owns data, loading, error, empty states
export function TaskListContainer() {
const { tasks, isLoading, error, refetch } = useTasks();
if (isLoading) return <TaskListSkeleton />;
if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
if (tasks.length === 0) return <EmptyState message="No tasks yet" />;
return <TaskList tasks={tasks} />;
}
// Presentational: pure rendering, easily testable, reusable
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul role="list" className="divide-y">
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}
Error Boundary with retry — any subtree that fetches/renders remote data MUST be wrapped in an Error Boundary that exposes a retry action. An inline if (error) return ... is NOT an Error Boundary (it can't catch render-time throws); you need both. Use react-error-boundary — the canonical one-pass pattern:
import { ErrorBoundary } from 'react-error-boundary';
// Fallback gets the error + a reset fn that re-mounts the boundary's children
function TaskListError({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" className="rounded border border-danger bg-surface p-4">
<p className="text-danger">Failed to load tasks: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}
// Wrap the data-fetching subtree. onReset clears stale state so retry refetches.
export function TaskListSection() {
return (
<ErrorBoundary FallbackComponent={TaskListError} onReset={() => { /* invalidate/refetch */ }}>
<Suspense fallback={<TaskListSkeleton />}>
<TaskListContainer />
</Suspense>
</ErrorBoundary>
);
}
Controlled vs uncontrolled — use uncontrolled (refs, FormData) for simple submit-only forms. Use controlled (useState) when you need: real-time validation, conditional fields, or derived values.
Validation UX rules:
Form patterns with useActionState + progressive enhancement -- forms should work without JS via standard form submission, then enhance with useActionState for inline validation and optimistic UI. Always: <form action={formAction}> not onSubmit. Server validates regardless of client. Display field-level errors from server response. Reset form state on success.
useActionState forms, useOptimistic optimistic UI, use() promises/context; ref is regular prop (no forwardRef)"use client" only for interactivity/browser APIsuseStore(s => s.count), NEVER destructure full store — unnecessary re-rendersuseSuspenseQuery + <Suspense fallback={<Skeleton/>}>. NOT isLoading early returnsif (isLoading) return <Spinner/> — use if (loading && !data) to avoid flash when data existsapi/ layer. All responses typedaria-busy="true" on loading regionsSemantic HTML landmarks first, ARIA second -- use <main> (once), <header>, <footer>, <nav>, <aside> for page landmarks. <article> for self-contained content, <section> for thematic groups with heading. <figure>/<figcaption> for media. Heading levels (h1-h6) must never skip levels. ARIA roles only when no semantic HTML equivalent exists.
Keyboard navigation — every interactive element must work with keyboard:
<button onClick={handleClick}>Click me</button> // Focusable by default
<div onClick={handleClick}>Click me</div> // NOT focusable — never do this
// If you must use a non-semantic element (you rarely must):
<div role="button" tabIndex={0} onClick={handleClick}
onKeyDown={e => e.key === 'Enter' && handleClick()}>
ARIA labels — label interactive elements that lack visible text:
<button aria-label="Close dialog"><XIcon /></button>
<input aria-label="Search tasks" type="search" />
// Prefer visible <label htmlFor="email"> over aria-label when possible
Focus management:
WCAG 2.1 AA:
prefers-reduced-motion: disable transforms, opacity onlyMobile-first breakpoints — start with the smallest screen, add complexity upward:
// Tailwind: mobile-first responsive
<div className="
grid grid-cols-1 /* Mobile: single column */
sm:grid-cols-2 /* ≥640px: 2 columns */
lg:grid-cols-3 /* ≥1024px: 3 columns */
gap-4
">
Test at these breakpoints: 320px, 768px, 1024px, 1440px.
CSS container queries for component-based responsive design -- use @container instead of @media for components that must adapt to their parent, not the viewport. Define containers: container-type: inline-size. Query them: @container (min-width: 400px) { ... }. Tailwind: @container / @lg:. Components become portable across layouts without media query rewrites. Use @media only for page-level layout shifts.
features/{name}/api/, components/, hooks/, types/, index.tscomponents/ui/, layouts components/layout/. @/ = src/./types/../types), NOT global @/types/ dump — if you see a type imported from a global @/types/... path, move it to the feature folderany, import type, JSDoc public APIs. Folder-based routing, lazy-load routesclamp(1rem, 0.5rem + 1vw, 1.5rem)text-primary, bg-surface, border-default) — never raw hex in components@layeranchor() function positions elements relative to another element without JS. Replaces Floating UI/Popper for most tooltip/popover cases. position-anchor: --my-trigger; top: anchor(bottom); left: anchor(center);. Progressive enhancement: fall back to fixed positioning.@tanstack/react-virtual (useVirtualizer); if a mapped list's length is unbounded, assume it can grow long and virtualize. No barrel imports; direct importsPromise.all() independent ops, start early/await late. Debounce search 300-500ms, cleanup effectsnext/dynamic, NEVER delete it and NEVER describe it in prose. Embody the import in the code:
const ProductChart = dynamic(() => import('./ProductChart'), { ssr: false, loading: () => <Skeleton /> });
The component stays a separate module — dynamic(() => import('./X')) references it without inlining its body. Lazy-load routes too. Core Web Vitals (LCP/INP/CLS)manifest.json (name, icons 192+512, display: standalone, theme_color), a service worker with cache-first for static assets + network-first for API, and <meta name='theme-color'>. Test with DevTools Application panel. This is progressive enhancement -- costs nothing if unused, massive UX win when installed.document.startViewTransition() for page-level transitions instead of wrapping everything in AnimatePresence. Browser-native, works with any framework, costs 0 JS bundle. Define transition names via view-transition-name CSS property. Combine with @media (prefers-reduced-motion: reduce) to disable.These are the rules most often missed. Each is a concrete IF → THEN. Scan your draft against every trigger; if the trigger fires and you didn't do the THEN, fix it before output.
'use client' at top of a file → only keep it if the file itself uses hooks/event handlers/browser APIs. If the page is mostly data display, split: keep the interactive leaf as the only 'use client' component, make the page/shell a server component. Never blanket-mark a whole page client.@/types/... (global dump) → colocate it: define/import the type from the feature folder (e.g. ./types or ../types), not a global types/. If only an import line is shown, change it to a feature-local path and define the type inline if needed.if (isLoading) return <Spinner/> / if (loading) ... early return → replace with Suspense-first: useSuspenseQuery + <Suspense fallback={<Skeleton/>}>. If you must keep a flag, gate it if (loading && !data) so cached data never flashes. The bare if (isLoading) return early-return is the trap — remove it.onChange updates query/filter state every keystroke → debounce 300–500ms (useDebouncedValue, useDeferredValue, or a setTimeout cleanup). Raw onChange={e => setSearch(e.target.value)} driving a filter/fetch is the trap.fetch(...) written inside a component or useEffect → move it to the feature api/ layer and call it via a typed function (and a query hook). No inline fetch/await fetch in component bodies — this includes form submit handlers, not just data loads.let, no hooks/refs declared outside a component.onClick that is not a native <button>/<a> (e.g. <div>, <article>, <li> onClick) → make it a real <button>, OR add ALL of: role="button", tabIndex={0}, and an onKeyDown handling Enter/Space. A clickable non-button without keyboard support is the trap — applies to clickable cards/rows too.{ ErrorBoundary } from react-error-boundary, give it a FallbackComponent whose fallback renders a Retry button wired to resetErrorBoundary, and an onReset that refetches. See the canonical "Error Boundary with retry" example above. An inline if (error) return ... is NOT an Error Boundary (it can't catch render throws) — you need the actual <ErrorBoundary> wrapper AND a retry button..map over fetched/filtered data with no fixed small cap) → virtualize with @tanstack/react-virtual (useVirtualizer). When item count is unbounded, assume it can be long and virtualize.next/dynamic, JAMAIS le supprimer ni le décrire en prose. Replace the static import with const X = dynamic(() => import('./X'), { ssr: false, loading: () => <Skeleton/> }) and keep rendering <X .../> exactly where it was. Deleting the component, or writing "would use next/dynamic" / "à loader si lourd" instead of the actual dynamic(() => import(...)) call, is the trap. The component remains its own module — you do NOT inline its body.Escape (keydown listener, cleaned up on unmount), and (b) move focus into the dialog (close button or first focusable element) on open. Overlay-click-only close is the trap. Then add the two best-practice mechanisms for a complete dialog: trap Tab focus inside (Tab/Shift+Tab cycle within, never escape behind) and restore focus to the trigger element on close (save document.activeElement on open, .focus() it on cleanup).After building UI: