-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
Create reusable Card and Button components with proper click handling, building on the experimental work done in PostCard. This supports the router migration (#200) by enabling mouse-driven navigation alongside keyboard controls.
Context
Related to #165 (modular component library) and follows the TanStack Router validation spike (#200). As we move toward a more centralized navigation architecture, clickable components become essential for mouse-driven navigation.
Experimental Findings
An experiment was conducted in PostCard.tsx to understand OpenTUI's mouse event capabilities. Key learnings:
1. OpenTUI Has Full Mouse Support
OpenTUI core (@opentui/core) provides comprehensive mouse event handlers through RenderableOptions:
onMouse?: (event: MouseEvent) => void // Catch-all handler
onMouseDown?: (event: MouseEvent) => void
onMouseUp?: (event: MouseEvent) => void
onMouseMove?: (event: MouseEvent) => void
onMouseDrag?: (event: MouseEvent) => void
onMouseDragEnd?: (event: MouseEvent) => void
onMouseDrop?: (event: MouseEvent) => void
onMouseOver?: (event: MouseEvent) => void
onMouseOut?: (event: MouseEvent) => void
onMouseScroll?: (event: MouseEvent) => void2. React Integration Works Via Pass-Through
The React reconciler passes unknown props directly to the underlying Renderable:
// In @opentui/react reconciler - unknown props flow through
default:
instance[propKey] = propValueThis means mouse handlers work on any OpenTUI React component without modification.
3. Click vs Drag Detection Required
Using onMouseDown alone causes clicks to fire during text selection. Solution: track mouse movement between down and up events.
const DRAG_THRESHOLD = 3; // pixels
const handleMouse = (event: MouseEvent) => {
if (event.type === "down") {
dragState.current = { isDragging: false, startX: event.x, startY: event.y };
} else if (event.type === "drag") {
const dx = Math.abs(event.x - dragState.current.startX);
const dy = Math.abs(event.y - dragState.current.startY);
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
dragState.current.isDragging = true;
}
} else if (event.type === "up") {
if (!dragState.current.isDragging) {
onClick(); // Only fire if no significant movement
}
}
};4. Event Propagation Works as Expected
event.stopPropagation() prevents parent handlers from firing, enabling nested clickable elements (e.g., like button inside card).
5. Hover States Work
onMouseOver and onMouseOut work for hover effects. State-based color changes provide visual feedback since terminal cursor styling isn't controllable by applications.
6. Terminal Cursor Limitation
Unlike web CSS cursor: pointer, terminal applications cannot change the mouse pointer appearance. The pointer is controlled by the terminal emulator, not the application. Hover color changes are the primary visual feedback mechanism.
Proposed Components
<Card> Component
A clickable container with optional hover highlighting:
interface CardProps {
children: React.ReactNode;
onClick?: () => void;
selected?: boolean;
hoverHighlight?: boolean;
style?: BoxProps["style"];
}
// Usage
<Card onClick={() => navigate("postDetail", { tweet })} selected={isSelected}>
<PostContent post={post} />
</Card><Button> Component
A clickable action element with hover states:
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: "default" | "primary" | "danger";
disabled?: boolean;
}
// Usage
<Button onClick={handleLike} variant="danger">
{isLiked ? "♥" : "♡"}
</Button>useClickHandler Hook
Extract the click vs drag detection logic into a reusable hook:
function useClickHandler(onClick?: () => void) {
const dragState = useRef({ isDragging: false, startX: 0, startY: 0 });
const handleMouse = onClick ? (event: MouseEvent) => {
// ... click vs drag logic
} : undefined;
return { onMouse: handleMouse };
}Tasks
- Extract
useClickHandlerhook from PostCard - Create
Cardcomponent with click and hover support - Create
Buttoncomponent for action icons - Refactor
PostCardto use new components - Add hover highlight option to Card
- Document mouse event patterns in component library
Files to Create/Modify
src/components/Card.tsx(new)src/components/Button.tsx(new)src/hooks/useClickHandler.ts(new)src/components/PostCard.tsx(refactor to use Card/Button)src/components/index.ts(exports)
Success Criteria
- Card and Button components are reusable across screens
- Click handling works correctly (no false triggers on text selection)
- Hover states provide visual feedback
- PostCard uses the new components without regression
- Components are documented with usage examples
Related
- Parent: refactor(components): Create more modular and reusable component library #165 (modular component library)
- Prior work: feat(router): TanStack Router validation spike #200 (TanStack Router validation spike)
- May support: feat(nav): Add global number key navigation and repurpose Tab for in-screen tabs #188 (global navigation), feat(nav): Add URL input to open tweets/profiles from x.com links #194 (URL input navigation)