en un clic
production-ready
// Review a component for production readiness as a UI framework. Assesses code quality, patterns, tests, docs, and API design. Produces a prioritized fix plan. Use before shipping a new or updated component to users.
// Review a component for production readiness as a UI framework. Assesses code quality, patterns, tests, docs, and API design. Produces a prioritized fix plan. Use before shipping a new or updated component to users.
Guide for creating a new UI component in @gridland/ui. Covers file structure, focus integration, keyboard handling, theme usage, JSDoc, export registration, and documentation.
Update context files to reflect current codebase state — new components, changed APIs, new patterns, and reasoning behind non-obvious decisions. Routes updates to the correct file based on what changed. Run after any significant design change before committing.
Audit the browser rendering pipeline for scissor/clipping bypass bugs. Use when touching browser-buffer.ts, canvas-painter.ts, or adding new visual features.
Diagnose layout issues in Gridland components. Spawns the layout-debugger agent with the relevant component. Use when a component renders incorrectly or layout looks wrong.
Pre-release checklist. Runs all 4 agents plus snapshot regression test, TypeScript check, and semver confirmation. Run before publishing a new package version.
Documentation-focused review. Runs docs-mirror and dependency-auditor in parallel. Use after writing or updating documentation, demo components, or MDX pages.
| name | production-ready |
| description | Review a component for production readiness as a UI framework. Assesses code quality, patterns, tests, docs, and API design. Produces a prioritized fix plan. Use before shipping a new or updated component to users. |
Production-readiness review for a @gridland/ui component. Evaluates whether a component is ready to ship to framework consumers who will add it to their own repos.
The user may specify a component name (e.g., /production-ready MultiSelect) or you can infer it from recent changes. If ambiguous, ask.
Determine the target component and gather all relevant files:
# If no component specified, infer from recent changes
git diff --name-only HEAD
Read all files for the target component:
packages/ui/components/<name>/<name>.tsx — implementationpackages/ui/components/<name>/*.test.tsx — all test filespackages/docs/content/docs/components/<name>.mdx — documentationpackages/demo/demos/<name>.tsx — demo (if exists)packages/docs/components/demos/<name>-demo.tsx — docs demo wrapper (if exists)packages/docs/public/r/<name>.json — emitted registry item (auto-regenerated by bun run --cwd packages/ui build; its files[].content is a verbatim copy of the component source)packages/ui/components/index.ts — export barrel (grep for the component)First, separate the primary component from companion demo utilities. Files that exist to showcase or preview variants (e.g., SpinnerPicker, SpinnerShowcase) are demo utilities — not shipped components. Only apply the full P0–P2 checklist to the primary component. Demo utilities only need basic correctness (// @ts-nocheck if needed, no crashes). Do not review their docs, add Controls tables, or treat them as consumer-facing API.
Signs something is a demo utility:
*Picker, *Showcase, *Demopackages/demo/ or docs wrappers, not by consumers directlyThen classify the primary component. This is critical — the rules differ:
Container components (e.g., SideNav, Modal, ChatPanel) manage their own focus scope. They MUST:
useInteractive and attach focusRefFocusScope with selectableshortcuts option on useInteractive (don't reach for useShortcuts directly)useFocusBorderStyle or useFocusDividerStyle for visual affordanceEmbedded components (e.g., SelectInput, MultiSelect, PromptInput) receive useKeyboard as a prop and are focus-managed by a parent. They do NOT call useInteractive, focusRef, FocusScope, or useShortcuts. This is intentional — not a gap.
How to tell: If the component accepts a useKeyboard prop and calls useKeyboardContext(useKeyboardProp), it is embedded. If it imports useInteractive from @gridland/utils, it is a container.
Static components (e.g., Spinner, Ascii, Table) have no keyboard interaction. Focus rules don't apply.
Check the implementation against these criteria, ordered by severity:
API correctness for framework consumers
index.ts — check that every interface or type used in public props or utility function signatures is re-exportedinterface/type, verify every field is consumed by at least one component prop or internal logic path. Fields that exist in a data type but are never read by any component (e.g., a tool field on a step data type that no component renders) are dead API surface — they mislead consumers into thinking the field has an effect. Also check that type names are specific enough to avoid collisions (e.g., Step is too generic — prefer ChainOfThoughtStepData)useKeyboard JSDoc references @gridland/utils (not @opentui/react or internal paths)=== or Set.has, document the equality constraint (primitives recommended) or add a type bounduseRef guard)open/defaultOpen/onOpenChange (or any value/defaultValue/onChange triplet), trace ALL four prop combinations: (1) controlled only (open), (2) uncontrolled only (defaultOpen), (3) uncontrolled + callback (defaultOpen + onOpenChange), (4) controlled + callback (open + onOpenChange). For each, verify the callback fires AND internal state updates correctly. The pattern const setter = onChangeCallback ?? setInternalState is a known anti-pattern — it replaces the internal state updater with the callback instead of doing both. The correct pattern calls onChangeCallback?.() alongside setInternalState() (see Radix useControllableState for reference)onSubmit) accepts Promise<void> return types, trace the actual code path. Verify that documented semantics match implementation — e.g., if JSDoc says "clears on resolve, preserves on reject," confirm input is NOT cleared before the promise starts, only in the .then() resolve path. State mutations before await/.then() defeat the async contract.catch or reject handler is empty or has only a comment, flag it. Errors should be surfaced via an onError callback or equivalent — silent swallowing hides failures from consumersKeyboard handler correctness
useKeyboard handler that can change between renders must use a ref — this includes state variables (cursor, selected, submitted), derived arrays rebuilt each render (e.g., trigger lists extracted from children via Children.forEach), and context values. The test is simple: "can this value differ between the render that registered the handler and the render when the handler fires?" If yes, use a ref. Don't limit this check to "rapidly-changing" values — even values that change infrequently (like the active tab) cause bugs when the handler captures a stale closureuseKeyboard, useCallback, or useEffect whose body is empty or contains only a guard clause (if (disabled) return) with no actual logic. These are no-ops that bloat the component and mislead readers<input> path and an unfocused useKeyboard fallback), verify the logic is shared via a single function — not copy-pasted. Duplicated branches diverge silently over time// @ts-nocheck on component files using intrinsics
<box>, <text>, <span>), it must have // @ts-nocheck at line 1packages/docs/public/r/<name>.json inlines the source verbatimTheme compliance
useTheme() or propstextStyle() for bold/dim/inverse (never raw style keys)Export registration
packages/ui/components/index.tspackages/ui/CLAUDE.md component catalogRegistry sync
packages/docs/public/r/<name>.json is auto-generated by packages/ui/scripts/build-registry.ts. Never edit it manually. After any component source change, regenerate: bun run --cwd packages/ui buildfiles[].content. If the emitted JSON looks stale, the build step wasn't run.packages/ui/components/<name>/<name>.tsx:
from "@gridland/utils" or other external import must appear in the item's dependencies array in build-registry.tsfrom "@/registry/gridland/<something>" cross-reference must appear in the item's registryDependencies array (bare name — the emitter prepends @gridland/)shadcn add won't install the missing npm dep or won't recursively fetch the transitive registry item)Unnecessary indirection
useCallback wrappers that just forward to a prop callback with no transformation (e.g., useCallback((v) => { onChange?.(v) }, [onChange])). Pass the prop directly insteadisControlled guards after removing uncontrolled mode)Performance
useMemo for expensive derived state (Set creation from arrays, flatMap operations)useMemo dep arrays are correct (no missing deps, no over-deps)new Set() on every render)React key stability and data-driven value uniqueness
.map() calls are stable across re-rendersoptions: string[], items: Item[]), check what happens if two entries have the same display value or the same identity value. If the array values are used as both React keys AND internal routing values (e.g., <Tabs value={options[i]}>), duplicate strings will cause wrong-item-selected bugs. The fix is to use indices or unique IDs as internal values, keeping display labels separateEdge cases
TabBar wrapping Tabs, or a simple items prop wrapping a compound component), trace the wrapper's internal translation logic. Common bugs: using display strings as identity values, index-to-value mappings that break with duplicates, missing prop forwarding. Test the wrapper with adversarial inputs (duplicate values, out-of-range indices, empty arrays)ReactNode prop that gates a conditional branch (e.g., if (extra !== undefined)), verify the guard uses != null — not !== undefined. Consumers commonly pass condition ? <content> : null, and null !== undefined is true, causing empty render artifacts instead of nothinggetSuggestions, custom renderers), trace what happens when the override returns values that differ from built-in assumptions. For example, if the default path assumes a trigger character like @, verify the custom path still works with a different trigger. Hardcoded lastIndexOf("@") or similar assumptions that break custom usage are P1 bugs<box>, <text>, <span>, etc.) and framework hooks (useTheme, textStyle). Flag any direct use of document, window, browser DOM APIs, or terminal-specific APIs (e.g., raw ANSI codes). Components distributed via the registry must render in both terminal and browserTest coverage by code path
activeColor ?? theme.accent), test that passing a custom value exercises the non-default path. Props with code paths but no tests are invisible features that can silently regressuseKeyboard prop vs useKeyboardContext), test that the prop takes precedence over context// @ts-nocheck if using OpenTUI intrinsics--preload ../web/test/preload.tsit() block that has no expect() call or where the assertion doesn't actually verify the behavior described in the test name. A test named "preserves input on reject" must assert the input value, not just run through the flowReactNode or a union type, verify tests exercise it with different value shapes (string, JSX element, null). String-only tests miss rendering issues with nested elementsDocumentation — code examples are valid
Documentation — prose matches implementation
Documentation — API table and controls
errorMessage or similar customization props are documentedDocumentation — example coverage
children and standard layout props) should have a corresponding fenced code example in the MDXDemo validity and feature coverage
Read the equivalent sibling component (e.g., if reviewing MultiSelect, read SelectInput) to verify:
useKeyboard, onSubmit, disabled)If the component has a shadcn/ui equivalent (Table, Tabs, Select, Modal/Dialog, etc.), compare API expressiveness:
disabled on interactive sub-components (triggers, items, options), asChild composition patterns, orientation for directional components, and loop for wrap-around behavior. If shadcn's equivalent has a prop that covers a common use case and Gridland doesn't, flag it as P1 (not P2) — these are expected features, not nice-to-havesIf the component has an equivalent in ai-elements, do a line-by-line comparison:
useControllableState or similar, compare the implementation logic — not just the prop names. Different solutions to the same problem often reveal bugs in one versionchildren prop covers the same use case in TUI"complete" vs "done", "active" vs "running"). Flag any difference as either a deliberate divergence (document why) or an accidental driftisLast for step connectors), investigate how ai-elements avoids it — the Gridland version may have a leaky abstractionDraft the report internally. Do NOT show the user anything yet — you must self-verify first (Phase 6).
This phase exists because issues repeatedly slip through single-pass reviews. You MUST complete every step below before presenting the report to the user. The goal is to catch everything in ONE run.
Walk through EVERY numbered item in the Phase 3 checklist (1–17) and confirm each one was actually checked against the code. For each item, write a one-line internal note:
[checked — pass] or [checked — FAIL: <issue>] or [N/A — <reason>]If any item shows [not checked], go back and check it now. Do not skip items because they "probably" pass.
Re-read the component source file (packages/ui/components/<name>/<name>.tsx) one more time. On this read-through, focus specifically on:
interface and type — are they all exported from index.ts? Including props types for sub-components?useMemo/useCallback/useEffect — correct deps?{ children: ReactNode }) that should be named interfaces?Run this verification:
export in the component source filepackages/ui/components/index.ts to confirm it's re-exportedtype/interface, confirm it has a export type { ... } line in index.tsindex.tsFor each prop in the component's Props interface:
For each code example in the docs:
index.ts)For every issue found (P0, P1, P2):
Ask yourself: "If I run /production-ready <component> again after these fixes, will I find zero new issues?" If the answer is no, you missed something — go back and find it.
Only after completing Phase 6, output the final report:
## Production Readiness: <ComponentName>
### Architecture: [Container | Embedded | Static]
### P0 — Blocking
- [file:line] Issue — what to fix
### P1 — Significant
- [file:line] Issue — what to fix
### P2 — Polish
- [file:line] Issue — what to fix
### Passing
- [list of checks that passed]
### Fix Plan
Ordered list of changes to make, with file paths and what to change.
Estimated scope: [trivial | small | medium | large]
After presenting the report, ask: "Want me to make these fixes?"
--update-snapshots test script — that's a repo-wide issue, not component-specific.