| 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.
Phase 1 — Identify scope
Determine the target component and gather all relevant files:
git diff --name-only HEAD
Read all files for the target component:
packages/ui/components/<name>/<name>.tsx — implementation
packages/ui/components/<name>/*.test.tsx — all test files
packages/docs/content/docs/components/<name>.mdx — documentation
packages/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)
Phase 2 — Architecture classification
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:
- Named
*Picker, *Showcase, *Demo
- Cycles through variants or displays all options side-by-side
- Used by
packages/demo/ or docs wrappers, not by consumers directly
Then classify the primary component. This is critical — the rules differ:
Container components (e.g., SideNav, Modal, ChatPanel) manage their own focus scope. They MUST:
- Call
useInteractive and attach focusRef
- Wrap content in
FocusScope with selectable
- Register shortcuts via the
shortcuts option on useInteractive (don't reach for useShortcuts directly)
- Use
useFocusBorderStyle or useFocusDividerStyle for visual affordance
Embedded 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.
Phase 3 — Code review
Check the implementation against these criteria, ordered by severity:
P0 — Blocking (must fix before shipping)
-
API correctness for framework consumers
- Props interface has JSDoc on every prop
- Exported types are useful (not leaking internals)
- Types that users need to work with the component API (data constraints, item shapes, column info) are exported from
index.ts — check that every interface or type used in public props or utility function signatures is re-exported
- Dead fields in exported types: For each exported
interface/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)
- Generic type parameters: if compared by
=== or Set.has, document the equality constraint (primitives recommended) or add a type bound
- Controlled/uncontrolled: if both modes are supported, warn on mode switching (check for
useRef guard)
- Controllable state correctness: If a component accepts
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)
- Async callback contracts: If a callback prop (e.g.,
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
- Silent error swallowing: If a Promise
.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 consumers
-
Keyboard handler correctness
- Every value read inside a
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 closure
- Verify the handler reads from refs for any render-dependent value and closure-captures only for truly stable props (consistent with SelectInput/PromptInput pattern)
- All keyboard bindings documented in the Controls section of the docs
- Dead handlers: Flag any
useKeyboard, 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
- Duplicated handler logic: If the component has multiple code paths handling the same keys (e.g., a focused
<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
- If the component file uses OpenTUI intrinsic elements (
<box>, <text>, <span>), it must have // @ts-nocheck at line 1
- There is no separate registry copy to check — the emitted
packages/docs/public/r/<name>.json inlines the source verbatim
-
Theme compliance
- No hardcoded hex colors — all colors from
useTheme() or props
- Uses
textStyle() for bold/dim/inverse (never raw style keys)
-
Export registration
- Both runtime and type exports in
packages/ui/components/index.ts
- Listed in
packages/ui/CLAUDE.md component catalog
-
Registry sync
- The emitted
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 build
- Source is the single source of truth — the builder reads component files verbatim and inlines them into
files[].content. If the emitted JSON looks stale, the build step wasn't run.
- Registry dependency completeness: Read the component's imports in
packages/ui/components/<name>/<name>.tsx:
- Any
from "@gridland/utils" or other external import must appear in the item's dependencies array in build-registry.ts
- Any
from "@/registry/gridland/<something>" cross-reference must appear in the item's registryDependencies array (bare name — the emitter prepends @gridland/)
- Missing either one causes install-time failures for framework consumers (
shadcn add won't install the missing npm dep or won't recursively fetch the transitive registry item)
-
Unnecessary indirection
- Flag
useCallback wrappers that just forward to a prop callback with no transformation (e.g., useCallback((v) => { onChange?.(v) }, [onChange])). Pass the prop directly instead
- Flag intermediary state or refs that serve no purpose after a refactor (e.g., leftover
isControlled guards after removing uncontrolled mode)
P1 — Significant (should fix)
-
Performance
useMemo for expensive derived state (Set creation from arrays, flatMap operations)
useMemo dep arrays are correct (no missing deps, no over-deps)
- No unnecessary allocations in the render path (e.g.,
new Set() on every render)
-
React key stability and data-driven value uniqueness
- Keys in
.map() calls are stable across re-renders
- Scroll-windowed lists use absolute indices, not relative window indices
- Group/separator keys won't collide
- Duplicate values in user-provided arrays: When a component accepts an array prop (e.g.,
options: 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 separate
-
Edge cases
- Empty items array handled gracefully
- Single item works
- Disabled state blocks all interaction
- Component handles controlled prop changes (re-render sync)
- Cursor/index clamping when list size changes
- Convenience wrapper correctness: If the component provides both a full API and a simplified wrapper (e.g.,
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 nullability: For every optional
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 nothing
- Extension point edge cases: When a component offers override callbacks (e.g.,
getSuggestions, 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
- Cross-environment safety
- Verify the component uses only OpenTUI intrinsics (
<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 browser
P2 — Polish (nice to have)
-
Test coverage by code path
- For each conditional branch in the render function, verify a corresponding test exists. Enumerate branches explicitly: disabled rendering, focused vs unfocused, label present vs absent, error vs description vs neither, empty value vs filled, maxLength counter shown vs hidden
- Every prop with a fallback or default: If a prop has a fallback (e.g.,
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 regress
- Every prop-injection override: If a prop overrides a context value (e.g.,
useKeyboard prop vs useKeyboardContext), test that the prop takes precedence over context
- No duplicate tests (same setup + assertions)
- Shared test helpers for repeated patterns (e.g., mock keyboard setup)
- Test file has
// @ts-nocheck if using OpenTUI intrinsics
- Tests run with
--preload ../web/test/preload.ts
- Assertion-less tests: Flag any
it() 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 flow
- Tests that document bugs: Flag tests where comments explain why the behavior is wrong or surprising instead of testing the intended behavior. Tests should assert correct behavior — if a code path is buggy, fix the code, don't write a test that explains the bug
- Polymorphic prop shapes: When a prop accepts
ReactNode or a union type, verify tests exercise it with different value shapes (string, JSX element, null). String-only tests miss rendering issues with nested elements
-
Documentation — code examples are valid
- Every fenced code block in the MDX must pass all required props and use correct prop names. Cross-reference the component's Props interface with each example. A user who copies any example should get zero TypeScript errors
- If a prop was recently made required or removed, check every example — not just the main usage block
-
Documentation — prose matches implementation
- The page description, section headings, and explanatory copy must accurately reflect the current API. If the component was refactored (e.g., uncontrolled removed, prop renamed, mode dropped), the narrative must match
- Check for stale references to removed concepts (e.g., "controlled/uncontrolled modes" after uncontrolled was removed)
-
Documentation — API table and controls
- API reference table matches current props (no missing, no stale, correct defaults)
- Controls section lists all keyboard bindings
errorMessage or similar customization props are documented
-
Documentation — example coverage
- Every non-trivial prop (anything beyond
children and standard layout props) should have a corresponding fenced code example in the MDX
- If a feature exists but has no example, users won't discover it — flag it
-
Demo validity and feature coverage
- The demo file and docs demo wrapper must pass all required props for every state/variant rendered. If a prop was made required, verify every demo usage — not just the main demo
- Demo state arrays (pickers, variant lists) must provide required props or the parent must supply defaults
- The demo should showcase the component's key features, not just render a basic default instance. Check that distinctive props (alignment, color overrides, colSpan, compound sub-components like Footer/Caption) appear in at least one demo variant. A demo that only shows the default configuration is incomplete
Phase 4 — Cross-reference with sibling components, shadcn, and ai-elements
Read the equivalent sibling component (e.g., if reviewing MultiSelect, read SelectInput) to verify:
- Pattern consistency (same reducer style, same keyboard handling approach, same ref usage)
- Consistent prop naming (e.g., both use
useKeyboard, onSubmit, disabled)
- No divergence that would confuse framework consumers
If the component has a shadcn/ui equivalent (Table, Tabs, Select, Modal/Dialog, etc.), compare API expressiveness:
- For each common use case of that component type, can users achieve it with the current props?
- Examples: right-aligning a column, coloring a status cell, spanning columns, disabling a row
- Flag major expressiveness gaps where shadcn users would expect functionality that Gridland doesn't offer
- Standard props that shadcn/Radix expose: Check specifically for
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-haves
- This is about API design quality, not HTML/CSS parity — adapt the comparison to what makes sense for TUI
If the component has an equivalent in ai-elements, do a line-by-line comparison:
- State management: How does ai-elements handle controllable state? If it uses
useControllableState or similar, compare the implementation logic — not just the prop names. Different solutions to the same problem often reveal bugs in one version
- Exported types: Compare field-by-field. If ai-elements doesn't have a field that Gridland exports, ask whether that field is actually consumed by any component code. Dead fields are a sign of speculative API design
- Sub-components: Are there compound subcomponents (Footer, Tools, ActionMenu) that Gridland is missing for common use cases? For browser-only sub-components (images, badges, click handlers), verify that Gridland's
children prop covers the same use case in TUI
- Status/enum values: Compare status strings (e.g.,
"complete" vs "done", "active" vs "running"). Flag any difference as either a deliberate divergence (document why) or an accidental drift
- Prop abstractions: If ai-elements doesn't require a prop that Gridland does (e.g.,
isLast for step connectors), investigate how ai-elements avoids it — the Gridland version may have a leaky abstraction
- Flag gaps as API design suggestions (P2), not blockers — TUI and web have different requirements
Phase 5 — Draft report (internal, do NOT present yet)
Draft the report internally. Do NOT show the user anything yet — you must self-verify first (Phase 6).
Phase 6 — Self-verification (REQUIRED before presenting)
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.
Step 1: Checklist audit
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.
Step 2: Re-read the source file
Re-read the component source file (packages/ui/components/<name>/<name>.tsx) one more time. On this read-through, focus specifically on:
- Every
interface and type — are they all exported from index.ts? Including props types for sub-components?
- Every prop — does it have JSDoc? Is it in the docs API table?
- Every conditional branch — is there a test for it?
- Every
useMemo/useCallback/useEffect — correct deps?
- Any inline prop types (e.g.,
{ children: ReactNode }) that should be named interfaces?
Step 3: Cross-check exports
Run this verification:
- List every
export in the component source file
- For each export, grep
packages/ui/components/index.ts to confirm it's re-exported
- For each exported
type/interface, confirm it has a export type { ... } line in index.ts
- Flag any export that exists in the source but not in
index.ts
Step 4: Cross-check docs against code
For each prop in the component's Props interface:
- Verify it appears in the docs API Reference table with the correct type, default, and description
- Verify at least one code example uses it (for non-trivial props)
For each code example in the docs:
- Verify the imports are valid (exist in
index.ts)
- Verify all required props are passed
- Verify no removed/renamed props are used
Step 5: Verify fix plan completeness
For every issue found (P0, P1, P2):
- Confirm the fix plan has a concrete action item with a file path
- Confirm the fix plan actions will actually resolve the issue (not just describe it)
Step 6: Final gate
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.
Phase 7 — Present report
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?"
Important guidance
- Do NOT flag missing focus integration on embedded components — this is by design.
- Do NOT flag stale closures on stable props (disabled, maxCount, etc.) in keyboard handlers — this is the standard pattern across all components.
- Do NOT flag the
--update-snapshots test script — that's a repo-wide issue, not component-specific.
- DO compare against sibling components to calibrate expectations. If SelectInput does something the same way, it's a pattern, not a bug.
- DO think from the perspective of a developer adding this component to their project via the framework. What will confuse them? What will break silently?