| 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)
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
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?