-
Notifications
You must be signed in to change notification settings - Fork 0
feat(components): expand component library to production-ready set #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add 25+ production-ready UI components across 5 categories: **Layout Components:** - Container: Responsive wrapper with safe area support - Stack/HStack/VStack: Flexible layout primitives - Spacer: Flexible spacing component - Divider: Visual separator **Form Components:** - Select: Dropdown with modal picker - Checkbox: Checkbox with label/description - Switch: Animated toggle switch - TextArea: Multi-line text input - Radio: RadioGroup component - FormField: Form field wrapper with validation **Feedback Components:** - Modal/ConfirmDialog: Dialog overlays - Toast: Notification system - Loading/Spinner/Skeleton: Loading states - Progress: ProgressBar, ProgressCircle, StepsProgress - BottomSheet/ActionSheet: Bottom modals **Display Components:** - Avatar/AvatarGroup: User avatars - Badge/StatusBadge: Labels and indicators - EmptyState/ErrorState/NoResults: Empty states - Tooltip/InfoTip: Contextual information **Code Components:** - DiffViewer: Code diff display - FileTree: File hierarchy navigation All components follow ThumbCode's organic "daube" styling with proper TypeScript types and accessibility attributes. Closes #13 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@coderabbitai full review |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (27)
📝 WalkthroughWalkthroughThis pull request significantly expands a React Native UI component library from 5 basic components to a comprehensive production-ready set across seven categories: code components (DiffViewer, FileTree), display components (Avatar, Badge, EmptyState, Tooltip), feedback components (BottomSheet, Modal, Toast, Loading, Progress), form components (Checkbox, RadioGroup, Select, Switch, TextArea, FormField), and layout components (Container, Stack, Divider, Spacer). Each file introduces new functional components with TypeScript prop interfaces and internal styling/animation logic. Barrel export files consolidate public APIs at module and root levels. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In `@src/components/code/DiffViewer.tsx`:
- Around line 66-73: The diff routine in DiffViewer (the block manipulating
result, oldLines, newLines, oldIndex, newIndex) only emits a 'remove' when
oldLines[oldIndex] !== newLines[newIndex], dropping the new line; fix by
emitting both the removal and the corresponding addition when lines differ (push
a {type: 'remove', ...} for oldLines[oldIndex] and a {type: 'add', ...} for
newLines[newIndex]) and advance both oldIndex and newIndex accordingly (or
implement a simple lookahead to detect pure insertions/removals); update the
logic inside the function that builds result (e.g., computeDiff / the loop that
uses oldIndex/newIndex) so new lines are not lost.
- Line 129: The inline style on the View in DiffViewer.tsx uses an invalid CSS
string for borderRadius; replace style={{ borderRadius: '12px 10px 14px 8px' }}
with numeric React Native corner props instead—use borderTopLeftRadius: 12,
borderTopRightRadius: 10, borderBottomRightRadius: 14, and
borderBottomLeftRadius: 8 (numbers, no "px") so the View's rounded corners
render correctly.
In `@src/components/code/FileTree.tsx`:
- Around line 187-201: The View in the FileTree component is using an invalid
string for borderRadius; replace the string `'12px 10px 14px 8px'` with numeric
React Native style properties (e.g. borderTopLeftRadius, borderTopRightRadius,
borderBottomRightRadius, borderBottomLeftRadius) on the same View where
sortedData.map renders FileTreeNode so the radii are numbers (no "px") and match
the intended asymmetry.
In `@src/components/display/Badge.tsx`:
- Around line 63-71: The Badge component's View sets style={{ borderRadius: '6px
8px 6px 10px' }}, which is invalid in React Native; change the style to use
numeric values and explicit corner properties (e.g. borderTopLeftRadius,
borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius) or a
single numeric borderRadius value in the View returned by Badge so the radii are
numbers instead of a CSS string.
In `@src/components/display/EmptyState.tsx`:
- Around line 70-86: The Pressable instances in EmptyState use an invalid string
borderRadius style ("8px 10px 8px 12px"); update the style on the Pressable(s)
that render secondaryAction and action to use React Native-compatible numeric
borderRadius values (e.g., borderRadius: 8) or implement per-corner numeric
properties (borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius,
borderBottomLeftRadius) to approximate the intended shape; adjust the
secondaryAction and action Pressable styles accordingly and remove the string
value so RN will honor the radius.
In `@src/components/display/Tooltip.tsx`:
- Around line 94-99: The borderRadius style on the Animated.View is using an
invalid CSS string ('8px 10px 8px 12px'); replace it with React Native numeric
corner properties (e.g. borderTopLeftRadius, borderTopRightRadius,
borderBottomRightRadius, borderBottomLeftRadius) using numbers (without 'px')
and merge them into the style array (the same place where borderRadius is
currently added), ensuring types line up with getTooltipStyle() and
Animated.View’s style prop.
- Around line 53-85: getTooltipStyle currently uses percentage strings and
numeric translate values (e.g., left: '50%' with transform: [{ translateX: -50
}]) which don't yield proper centering in React Native; modify the tooltip to
measure its own size via onLayout (capture measured width/height in state) and
update getTooltipStyle to compute numeric centering offsets: replace left:
'50%'/top: '50%' and translateX/Y -50 with numeric left/top values based on
layout.x/layout.y plus half of the target element, and use transform: [{
translateX: -measuredWidth/2 }] or translateY: -measuredHeight/2 as appropriate
in the getTooltipStyle function and the Animated.View that renders the tooltip
(referencing getTooltipStyle, position, layout and the new
measuredWidth/measuredHeight state).
In `@src/components/feedback/BottomSheet.tsx`:
- Around line 214-221: The inline style on the cancel Pressable uses an invalid
string for borderRadius ('12px 14px 12px 16px'); update the style in
BottomSheet.tsx (the Pressable that renders cancel button) to use numeric radius
values or explicit corner properties (e.g. replace style: { borderRadius: '12px
14px 12px 16px' } with an object like { borderTopLeftRadius: 12,
borderTopRightRadius: 14, borderBottomRightRadius: 12, borderBottomLeftRadius:
16 } or a single numeric borderRadius without 'px') so it conforms to React
Native style types.
- Around line 70-92: PanResponder created in the useRef (panResponder /
PanResponder.create) captures the initial onClose closure and becomes stale if
onClose changes; fix by storing the latest onClose in a mutable ref (e.g.,
onCloseRef.current) updated inside the component on each render and replace
direct calls to onClose inside onPanResponderRelease with onCloseRef.current();
ensure the ref is declared with useRef and updated before the PanResponder usage
so translateY and gesture logic remain unchanged.
In `@src/components/feedback/Loading.tsx`:
- Around line 90-105: The useEffect in Loading.tsx starts an Animated.loop but
never stops it on unmount; modify the effect that creates the loop (the
Animated.loop call using pulseAnim and Animated.timing) to capture the returned
animation instance (e.g., const animation = Animated.loop(...)), call
animation.start(), and return a cleanup function that stops the loop
(animation.stop()) so the pulseAnim loop is torn down when the component
unmounts; apply the same pattern used to fix Skeleton if present.
- Around line 132-142: The overlay uses an invalid string borderRadius and lacks
accessibility/interaction blocking; in the Loading component replace the string
style with numeric corner radii (e.g., borderTopLeftRadius: 16,
borderTopRightRadius: 14, borderBottomLeftRadius: 18, borderBottomRightRadius:
12) for the inner View, and make the outer overlay accessible and modal by
adding accessible={true}, accessibilityViewIsModal={true}, an accessibilityLabel
like message ?? 'Loading', and accessibilityLiveRegion="polite" (or
accessibilityRole="alert") so screen readers announce the loading state; ensure
the overlay blocks underlying interaction by keeping it covering the screen
(pointerEvents can remain default/"auto" on the outer View) and set
accessibilityElementsHidden={false} on the overlay and true on any background
content if needed.
- Around line 34-43: The effect creates an infinite Animated.loop with
Animated.timing and never stops it; update the useEffect that references
spinValue to capture the returned animation (const animation =
Animated.loop(...)), call animation.start(), and return a cleanup function that
calls animation.stop() (and optionally reset spinValue with
spinValue.setValue(0)) so the animation is properly stopped on component
unmount; locate this in the useEffect that creates the Animated.loop and uses
spinValue.
In `@src/components/feedback/Modal.tsx`:
- Around line 68-73: The inline style in Modal.tsx is using an invalid CSS
string for borderRadius ("20px 16px 20px 18px") which React Native doesn't
accept; update the style in the component (the style prop where isFull is used)
to use numeric radius properties: set borderRadius to 0 when isFull is true,
otherwise remove the string and instead set numeric values like
borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, and
borderBottomLeftRadius to the corresponding numbers (e.g., 20, 16, 20, 18) so
React Native receives numeric values.
- Around line 141-157: The Pressable buttons in Modal.tsx use an invalid string
value for style.borderRadius ('8px 10px 8px 12px') which React Native doesn't
accept; update both Pressable elements (the cancel and confirm buttons) to use
numeric border radius properties instead — either a single numeric borderRadius
(e.g., 8) or explicit side properties like borderTopLeftRadius,
borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius with
numeric values; apply the change to the style prop on both Pressable instances
(and consider moving these styles into a StyleSheet or shared constant for
consistency).
In `@src/components/form/Checkbox.tsx`:
- Around line 53-57: The inline style in the Checkbox component uses an invalid
CSS-style borderRadius string; update the style in Checkbox (where style={{
width: styles.box, height: styles.box, borderRadius: '4px 6px 4px 6px' }}) to
use React Native numeric corner properties instead—replace borderRadius with
borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, and
borderBottomLeftRadius set to the appropriate numeric values (e.g., 4 and 6) so
the component accepts the radii correctly (same approach used to fix
Tooltip.tsx).
In `@src/components/form/Select.tsx`:
- Around line 57-72: The Pressable used as the select trigger is missing
accessibility attributes and uses invalid string borderRadius values; update the
Pressable (and related props like setIsOpen/disabled/hasError/selectedOption) to
include accessibilityRole="button" (or "combobox" if your platform supports it),
accessibilityState={{ disabled, expanded: isOpen }}, and an accessibilityLabel
(e.g., selectedOption?.label || placeholder) so screen readers know purpose and
state, and replace the string borderRadius with numeric style properties
(borderTopLeftRadius, borderTopRightRadius, borderBottomLeftRadius,
borderBottomRightRadius) or a single numeric borderRadius value to fix the React
Native style error.
In `@src/components/form/TextArea.tsx`:
- Around line 74-80: The style object in the TextArea component is setting
borderRadius to an invalid CSS string; replace that string with numeric
per-corner React Native props (e.g., borderTopLeftRadius, borderTopRightRadius,
borderBottomRightRadius, borderBottomLeftRadius) using numbers instead of a
string and keep the other properties (minHeight, maxHeight, textAlignVertical,
lineHeight) unchanged so the style object is valid for React Native.
🟡 Minor comments (13)
src/components/code/FileTree.tsx-112-119 (1)
112-119: Status color may override selection color due to class order.When a file is both selected and has a status (e.g.,
modified), thestatusColorclass is appended after the selection color class. Depending on NativeWind's handling, this could cause the status color to override the selection highlight.Consider using separate Text elements or conditional logic to ensure both indicators are visible.
src/components/display/Avatar.tsx-44-51 (1)
44-51: Potential runtime error if name contains empty segments.If
namecontains consecutive spaces (e.g.,"John Doe"), splitting produces empty strings, and accessingpart[0]on an empty string returnsundefined, which would be rendered.🐛 Proposed fix
function getInitials(name: string): string { return name .split(' ') + .filter(Boolean) .map((part) => part[0]) .join('') .toUpperCase() .slice(0, 2); }src/components/layout/Spacer.tsx-29-38 (1)
29-38: Edge case:size={0}will be ignored due to falsy check.When
sizeis0, the expressionsize ? ... : undefinedevaluates toundefinedbecause0is falsy in JavaScript. This causes the component to use flex spacing instead of a zero-dimension spacer.If zero-spacing is a valid use case, consider using
size !== undefinedinstead.🐛 Proposed fix
export function Spacer({ size, flex = 1, direction = 'vertical' }: SpacerProps) { - const dimension = size ? (typeof size === 'number' ? size : sizeValues[size]) : undefined; + const dimension = size !== undefined ? (typeof size === 'number' ? size : sizeValues[size]) : undefined; const style: ViewStyle = { - flex: size ? undefined : flex, + flex: size !== undefined ? undefined : flex, width: direction === 'horizontal' ? dimension : undefined, height: direction === 'vertical' ? dimension : undefined, };src/components/display/Avatar.tsx-112-129 (1)
112-129: Weak key generation may cause React reconciliation issues.Using
avatar.name || indexas a key can lead to collisions when multiple avatars share the same name or are unnamed. This could cause incorrect rendering or state retention.Consider requiring a unique
idfield on avatar items, or use a combination of properties.♻️ Suggested interface update
interface AvatarGroupProps { /** List of avatars */ - avatars: Array<{ src?: string; name?: string }>; + avatars: Array<{ id: string; src?: string; name?: string }>; /** Maximum avatars to show before +N */ max?: number; /** Size of avatars */ size?: AvatarSize; } // Then in the map: -key={`${avatar.name || index}`} +key={avatar.id}src/components/display/Badge.tsx-50-61 (1)
50-61: Fragile string manipulation for dot background color.The
.replace('/20', '')approach assumes the background class always contains/20. If a variant without opacity is added (e.g.,bg-neutral-600), this replacement would have no effect, and the dot would retain the semi-transparent background.Consider storing the solid color separately in
variantStylesor using a more robust approach.♻️ Suggested improvement
-const variantStyles: Record<BadgeVariant, { bg: string; text: string }> = { - default: { bg: 'bg-neutral-600', text: 'text-neutral-200' }, - primary: { bg: 'bg-coral-500/20', text: 'text-coral-400' }, +const variantStyles: Record<BadgeVariant, { bg: string; bgSolid: string; text: string }> = { + default: { bg: 'bg-neutral-600', bgSolid: 'bg-neutral-600', text: 'text-neutral-200' }, + primary: { bg: 'bg-coral-500/20', bgSolid: 'bg-coral-500', text: 'text-coral-400' }, // ... other variants }; // Then in dot rendering: -className={colors.bg.replace('/20', '')} +className={colors.bgSolid}src/components/feedback/BottomSheet.tsx-139-141 (1)
139-141: SameshowsVerticalScrollIndicatorprop issue as Modal.When
scrollableis false,ContentWrapperis aViewwhich doesn't accept this prop.Use the same fix pattern as suggested for Modal.tsx - conditionally render ScrollView or View instead of dynamic component assignment.
src/components/feedback/Modal.tsx-52-53 (1)
52-53:showsVerticalScrollIndicatorpassed to View when not scrollable.When
scrollableisfalse,ContentWrapperis aView, which doesn't acceptshowsVerticalScrollIndicator. This will cause a React Native warning about unrecognized props.Proposed fix
- const ContentWrapper = scrollable ? ScrollView : View; + const contentProps = scrollable + ? { showsVerticalScrollIndicator: false } + : {}; // ... - <ContentWrapper className="px-5 py-4" showsVerticalScrollIndicator={false}> + {scrollable ? ( + <ScrollView className="px-5 py-4" showsVerticalScrollIndicator={false}> + {children} + </ScrollView> + ) : ( + <View className="px-5 py-4">{children}</View> + )} - {children} - </ContentWrapper>Also applies to: 86-86
src/components/feedback/BottomSheet.tsx-22-22 (1)
22-22:SCREEN_HEIGHTwon't update on device rotation.The screen height is captured once at module load time. If the device rotates,
SCREEN_HEIGHTwill be stale, causing incorrect height calculations for 'half' and 'full' presets.Suggested fix using useWindowDimensions
-import { - Animated, - Dimensions, - ... -} from 'react-native'; +import { + Animated, + useWindowDimensions, + ... +} from 'react-native'; -const { height: SCREEN_HEIGHT } = Dimensions.get('window'); export function BottomSheet({...}: BottomSheetProps) { + const { height: screenHeight } = useWindowDimensions(); // ... use screenHeight instead of SCREEN_HEIGHTsrc/components/form/Select.tsx-82-84 (1)
82-84:stopPropagation()may not reliably prevent backdrop dismiss in React Native.Unlike web, React Native's event system doesn't use DOM event bubbling.
e.stopPropagation()on a nestedPressablemay not prevent the parent'sonPressfrom firing in all cases. The current structure where the outer Pressable wraps content can lead to accidental dismissals.Suggested alternative using onStartShouldSetResponder
- <Pressable onPress={(e) => e.stopPropagation()}> + <View onStartShouldSetResponder={() => true}> <View className="bg-surface m-4 max-h-80 overflow-hidden" - style={{ borderRadius: '20px 16px 20px 18px' }} + style={{ borderRadius: 18 }} >Alternatively, restructure so the backdrop and content are siblings rather than parent-child.
src/components/code/DiffViewer.tsx-131-147 (1)
131-147: Add accessibility attributes to the collapsible header.The collapsible header
Pressableis missing accessibility attributes, which impacts screen reader users. Per PR objectives, accessibility support is a requirement.♿ Proposed accessibility improvements
<Pressable onPress={() => setCollapsed(!collapsed)} className="flex-row items-center justify-between px-3 py-2 bg-neutral-800 border-b border-neutral-700" + accessibilityRole="button" + accessibilityLabel={`${filename || 'Diff'}: ${additions} additions, ${deletions} deletions. ${collapsed ? 'Expand' : 'Collapse'}`} + accessibilityState={{ expanded: !collapsed }} >src/components/display/Tooltip.tsx-29-51 (1)
29-51: Missing cleanup for timeout on component unmount.If the component unmounts while the tooltip delay timer is pending,
setTimeoutwill still fire and attempt to update state on an unmounted component. Add cleanup in auseEffect.🐛 Add cleanup effect
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(); + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); const showTooltip = () => {src/components/feedback/Progress.tsx-123-171 (1)
123-171: ProgressCircle animation is a no-op (rotateAnim unused).
rotateAnimupdates but isn’t applied, so the indicator jumps instead of animating. Bind the animated value to rotation.✅ Suggested fix
useEffect(() => { Animated.timing(rotateAnim, { toValue: clampedValue, duration: 500, useNativeDriver: true, }).start(); }, [clampedValue, rotateAnim]); + const rotate = rotateAnim.interpolate({ + inputRange: [0, 100], + outputRange: ['0deg', '360deg'], + }); ... - <View + <Animated.View style={{ width: size, height: size, borderRadius: size / 2, borderWidth: strokeWidth, borderColor: circleColors[color], borderLeftColor: 'transparent', borderBottomColor: 'transparent', - transform: [{ rotate: `${(clampedValue / 100) * 360}deg` }], + transform: [{ rotate }], }} />src/components/feedback/Progress.tsx-203-209 (1)
203-209: Use per-corner radii; React Native doesn't support multi-valueborderRadiusshorthand.The
borderRadiusvalue with multiple space-separated values (like'8px 10px 8px 12px') is invalid in React Native and will be ignored. Use individual corner radius properties instead.✅ Suggested fix
<View className={`w-8 h-8 items-center justify-center ${ isCompleted ? 'bg-teal-600' : isCurrent ? 'bg-coral-500' : 'bg-neutral-700' }`} - style={{ borderRadius: '8px 10px 8px 12px' }} + style={{ + borderTopLeftRadius: 8, + borderTopRightRadius: 10, + borderBottomRightRadius: 8, + borderBottomLeftRadius: 12, + }} >
🧹 Nitpick comments (19)
src/components/form/FormField.tsx (1)
36-48: Consider adding accessibility attributes to the label.The label row renders correctly, but for screen reader support (as mentioned in PR objectives), consider adding
accessibilityRole="text"or associating the label with the input usingaccessibilityLabelledBy/nativeIDpatterns.♻️ Suggested accessibility enhancement
{(label || labelRight) && ( - <View className="flex-row items-center justify-between mb-1.5"> + <View className="flex-row items-center justify-between mb-1.5" accessibilityRole="text"> {label && ( - <Text className="font-body text-sm text-neutral-300"> + <Text className="font-body text-sm text-neutral-300" nativeID="field-label"> {label} {required && <Text className="text-coral-500 ml-0.5">*</Text>} </Text>src/components/layout/Divider.tsx (1)
43-58: Potential style conflict when usingclassNamewith background color.The
styleprop setsbackgroundColordirectly, whileclassNamemay also contain background color classes. Inline styles take precedence in React Native, so anybg-*class inclassNamewould be overridden. Consider documenting this behavior or removingbackgroundColorfrom style when a customclassNameis provided.Additionally, the vertical orientation uses
height: '100%', which requires the parent container to have an explicit height to render correctly.src/components/code/FileTree.tsx (1)
148-155:defaultExpandedonly initializes state; prop changes are ignored.This is a common controlled/uncontrolled pattern, but if consumers expect to programmatically expand folders by updating
defaultExpanded, it won't work. This behavior is acceptable but should be documented, or consider adding anexpandedcontrolled prop.src/components/display/Badge.tsx (1)
25-32:primaryanderrorvariants have identical styles.Both variants use
bg-coral-500/20andtext-coral-400. If this is intentional for the design system, consider adding a comment. Otherwise, differentiate them for clearer semantic meaning.src/components/display/EmptyState.tsx (1)
51-65: Add accessibility attributes to the container.Per PR objectives requiring accessibility features, consider adding
accessibilityRole="alert"oraccessibilityRole="status"to the container View for screen reader users, especially forErrorStateandNoResultsvariants.Suggested improvement
<View className={`items-center justify-center ${styles.padding}`} + accessibilityRole="status" + accessibilityLabel={`${title}. ${description || ''}`} >src/components/form/Select.tsx (2)
94-118: Add accessibility to option items.Each option should have appropriate accessibility attributes for screen reader users.
Suggested improvement
<Pressable onPress={() => !item.disabled && handleSelect(item.value)} disabled={item.disabled} className={`px-4 py-3 border-b border-neutral-800 ${ item.value === value ? 'bg-teal-600/20' : '' } ${item.disabled ? 'opacity-50' : 'active:bg-neutral-700'}`} + accessibilityRole="radio" + accessibilityState={{ + selected: item.value === value, + disabled: item.disabled + }} + accessibilityLabel={item.label} >
192-194: Consider using a stable key instead ofoption.label.If two options share the same label (edge case), this will cause React key warnings. Using
item.value(already used in keyExtractor for FlatList) or an index fallback would be more robust.- <Pressable - key={option.label} + <Pressable + key={`${option.label}-${index}`}src/components/layout/Container.tsx (1)
68-76:flex: 1default may cause unexpected layout behavior.The container always has
flex: 1, which causes it to expand to fill available space. This might be unexpected when usingsizeconstraints or when the container is meant to shrink to content. Consider making this configurable or removing it for constrained sizes.Suggested improvement
const containerStyle: ViewStyle = { - flex: 1, + flex: size === 'full' ? 1 : undefined, padding: paddingValue, maxWidth, width: '100%', alignSelf: center ? 'center' : undefined, ...safeAreaStyle, ...style, };Alternatively, add a
flexprop to let consumers control this behavior.src/components/feedback/Loading.tsx (1)
53-67: Add accessibility attributes to Spinner.Loading indicators should announce their state to screen readers.
Suggested improvement
<View className="items-center" + accessibilityRole="progressbar" + accessibilityLabel={label || "Loading"} + accessibilityState={{ busy: true }} >src/components/feedback/Modal.tsx (1)
54-55: AddaccessibilityViewIsModalfor proper screen reader behavior.When a modal is visible, screen readers should focus trap within the modal content.
Suggested improvement
<RNModal visible={visible} transparent animationType="fade" onRequestClose={onClose} + accessibilityViewIsModal={true} >src/components/feedback/BottomSheet.tsx (2)
192-212: Add accessibility attributes to action options.Each option should have appropriate accessibility markup.
Suggested improvement
<Pressable key={option.label} onPress={() => { option.onPress(); onClose(); }} disabled={option.disabled} className={`py-4 ${index > 0 ? 'border-t border-neutral-800' : ''} ${ option.disabled ? 'opacity-50' : 'active:bg-neutral-800' }`} + accessibilityRole="button" + accessibilityState={{ disabled: option.disabled }} + accessibilityLabel={option.label} >
53-67: Animation cleanup on unmount needed.The spring and timing animations should be stopped when the component unmounts or visibility changes to prevent memory leaks.
Suggested improvement
useEffect(() => { + let animation: Animated.CompositeAnimation; if (visible) { - Animated.spring(translateY, { + animation = Animated.spring(translateY, { toValue: 0, useNativeDriver: true, tension: 50, friction: 10, - }).start(); + }); } else { - Animated.timing(translateY, { + animation = Animated.timing(translateY, { toValue: SCREEN_HEIGHT, duration: 200, useNativeDriver: true, - }).start(); + }); } + animation.start(); + return () => animation.stop(); }, [visible, translateY]);src/components/code/DiffViewer.tsx (1)
84-88: Consider documenting or removing the unimplementedviewModeprop.The
viewModeprop is accepted but immediately voided. While thevoid _viewModepattern suppresses the unused variable warning, consumers may expectsplitmode to work.Consider either:
- Removing the prop until implemented
- Adding a console warning when
splitis passed- Documenting clearly that only
unifiedis currently supportedsrc/components/form/Radio.tsx (1)
51-52: Consider addingaccessibilityRole="radiogroup"to the container.The individual radio buttons have proper accessibility attributes, but the containing
Viewshould haveaccessibilityRole="radiogroup"to properly convey the group semantics to screen readers.♿ Proposed accessibility improvement
- <View className="w-full"> + <View className="w-full" accessibilityRole="radiogroup" accessibilityLabel={label}>src/components/form/Switch.tsx (2)
45-52: Optimize useEffect dependency to avoid unnecessary re-renders.The
stylesobject is recreated on every render (derived fromsizeStyles[size]), causing the effect to re-run even when only the object reference changes but values remain the same. Usesizedirectly in the dependency array instead.♻️ Suggested optimization
useEffect(() => { + const styles = sizeStyles[size]; Animated.spring(translateX, { toValue: value ? styles.width - styles.thumb - styles.offset * 2 : 0, useNativeDriver: true, tension: 50, friction: 8, }).start(); - }, [value, translateX, styles]); + }, [value, translateX, size]);
55-60: Redundant disabled check in onPress handler.The
disabledprop is already passed toPressable, which prevents press events when disabled. The!disabled &&check inonPressis redundant.♻️ Minor cleanup
<Pressable - onPress={() => !disabled && onValueChange(!value)} + onPress={() => onValueChange(!value)} disabled={disabled}src/components/form/TextArea.tsx (1)
58-81: Consider adding accessibility label for the TextInput.Other form components in this PR include accessibility attributes. For consistency and screen reader support, consider adding
accessibilityLabelthat references the label text.♻️ Add accessibility support
<TextInput {...props} value={value?.toString()} onChangeText={onChangeText} multiline maxLength={maxLength} + accessibilityLabel={label} onFocus={(e) => {src/components/feedback/Toast.tsx (1)
83-120: Exit animation won’t render because the view is unmounted.When
visibleis false you returnnull, so the slide/fade-out never paints. If you intend an exit animation, keep the view mounted until the hide animation completes.💡 One way to keep it mounted for the hide animation
-import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; ... - useEffect(() => { + const [rendered, setRendered] = useState(visible); + + useEffect(() => { + if (visible) setRendered(true); + }, [visible]); + + useEffect(() => { if (visible) { Animated.parallel([ Animated.spring(translateY, { toValue: 0, useNativeDriver: true, tension: 50, friction: 10, }), Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); if (duration > 0) { const timer = setTimeout(onDismiss, duration); return () => clearTimeout(timer); } } else { Animated.parallel([ Animated.timing(translateY, { toValue: position === 'top' ? -100 : 100, duration: 200, useNativeDriver: true, }), Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true, }), ]).start(); + const hideTimer = setTimeout(() => setRendered(false), 200); + return () => clearTimeout(hideTimer); } - }, [visible, duration, onDismiss, translateY, opacity, position]); + }, [visible, duration, onDismiss, translateY, opacity, position]); - if (!visible) return null; + if (!rendered) return null;src/components/layout/Stack.tsx (1)
9-75: BroadenstyletoStylePropand merge safely.
style?: ViewStyledisallows common RN usage (arrays, registered styles). Also, spreading...stylecan break if a consumer passes an array. ConsiderStyleProp<ViewStyle>andstyle={[stackStyle, style]}.♻️ Suggested refactor
-import { View, type ViewStyle } from 'react-native'; +import { View, type ViewStyle, type StyleProp } from 'react-native'; ... - style?: ViewStyle; + style?: StyleProp<ViewStyle>; ... const stackStyle: ViewStyle = { flexDirection: direction, gap, alignItems: alignMap[align], justifyContent: justifyMap[justify], flexWrap: wrap ? 'wrap' : 'nowrap', flex, - ...style, }; ... - return ( - <View style={stackStyle} className={className}> + return ( + <View style={[stackStyle, style]} className={className}> {children} </View> );
| } else { | ||
| result.push({ | ||
| type: 'remove', | ||
| content: oldLines[oldIndex], | ||
| oldLineNumber: oldIndex + 1, | ||
| }); | ||
| oldIndex++; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diff algorithm misses additions when lines differ.
When oldLines[oldIndex] !== newLines[newIndex], the current logic only removes the old line without ever adding the new line. This causes insertions to be lost when content changes rather than being purely appended.
For example, comparing "A" with "B" produces only [{type: 'remove', content: 'A'}] but never adds "B".
🐛 Proposed fix to handle additions when lines differ
} else {
+ // Remove old line
result.push({
type: 'remove',
content: oldLines[oldIndex],
oldLineNumber: oldIndex + 1,
});
oldIndex++;
+ // Add new line
+ result.push({
+ type: 'add',
+ content: newLines[newIndex],
+ newLineNumber: newIndex + 1,
+ });
+ newIndex++;
}Note: As the comment on line 34 suggests, consider using a proper diff library (e.g., diff or jsdiff) for production use to handle complex cases like moved blocks and provide optimal diffs.
🤖 Prompt for AI Agents
In `@src/components/code/DiffViewer.tsx` around lines 66 - 73, The diff routine in
DiffViewer (the block manipulating result, oldLines, newLines, oldIndex,
newIndex) only emits a 'remove' when oldLines[oldIndex] !== newLines[newIndex],
dropping the new line; fix by emitting both the removal and the corresponding
addition when lines differ (push a {type: 'remove', ...} for oldLines[oldIndex]
and a {type: 'add', ...} for newLines[newIndex]) and advance both oldIndex and
newIndex accordingly (or implement a simple lookahead to detect pure
insertions/removals); update the logic inside the function that builds result
(e.g., computeDiff / the loop that uses oldIndex/newIndex) so new lines are not
lost.
| <Pressable | ||
| onPress={onClose} | ||
| className="px-4 py-2 bg-neutral-700 active:bg-neutral-600" | ||
| style={{ borderRadius: '8px 10px 8px 12px' }} | ||
| > | ||
| <Text className="font-body text-white">{cancelText}</Text> | ||
| </Pressable> | ||
| <Pressable | ||
| onPress={() => { | ||
| onConfirm(); | ||
| onClose(); | ||
| }} | ||
| className={`px-4 py-2 ${confirmColor} active:opacity-80`} | ||
| style={{ borderRadius: '8px 10px 8px 12px' }} | ||
| > | ||
| <Text className="font-body text-white font-semibold">{confirmText}</Text> | ||
| </Pressable> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix borderRadius on ConfirmDialog buttons.
Proposed fix
<Pressable
onPress={onClose}
className="px-4 py-2 bg-neutral-700 active:bg-neutral-600"
- style={{ borderRadius: '8px 10px 8px 12px' }}
+ style={{ borderRadius: 10 }}
+ accessibilityRole="button"
>
<Text className="font-body text-white">{cancelText}</Text>
</Pressable>
<Pressable
onPress={() => {
onConfirm();
onClose();
}}
className={`px-4 py-2 ${confirmColor} active:opacity-80`}
- style={{ borderRadius: '8px 10px 8px 12px' }}
+ style={{ borderRadius: 10 }}
+ accessibilityRole="button"
>🤖 Prompt for AI Agents
In `@src/components/feedback/Modal.tsx` around lines 141 - 157, The Pressable
buttons in Modal.tsx use an invalid string value for style.borderRadius ('8px
10px 8px 12px') which React Native doesn't accept; update both Pressable
elements (the cancel and confirm buttons) to use numeric border radius
properties instead — either a single numeric borderRadius (e.g., 8) or explicit
side properties like borderTopLeftRadius, borderTopRightRadius,
borderBottomRightRadius, borderBottomLeftRadius with numeric values; apply the
change to the style prop on both Pressable instances (and consider moving these
styles into a StyleSheet or shared constant for consistency).
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
|
@claude In
In
In
In
In
In
In
In
In
In
In
|
- Fix DiffViewer diff algorithm to emit both 'remove' and 'add' when lines differ (was missing additions) - Convert all borderRadius string values to React Native compatible numeric corner radius properties across 13 component files Components updated: - DiffViewer: diff algorithm + borderRadius - FileTree: borderRadius - Badge: borderRadius - EmptyState: borderRadius (2 buttons) - Tooltip: borderRadius - BottomSheet: borderRadius - Loading: borderRadius - Modal: borderRadius (2 buttons) - Progress: borderRadius - Toast: borderRadius - Checkbox: borderRadius - Select: borderRadius (2 locations) - TextArea: borderRadius Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|


Summary
Expands the ThumbCode component library with 25+ production-ready UI components across 5 categories, providing a comprehensive foundation for mobile app development.
Components Added
Layout Components (4):
Container- Responsive wrapper with safe area supportStack/HStack/VStack- Flexible layout primitivesSpacer- Flexible spacing componentDivider- Visual separator with organic stylingForm Components (6):
Select- Dropdown with modal pickerCheckbox- Checkbox with label/description supportSwitch- Animated toggle switchTextArea- Multi-line text input with character countRadio/RadioGroup- Radio button groupFormField- Form field wrapper with validation displayFeedback Components (8):
Modal/ConfirmDialog- Dialog overlays with organic stylingToast- Notification systemLoading/Spinner/Skeleton/LoadingOverlay- Loading statesProgressBar/ProgressCircle/StepsProgress- Progress indicatorsBottomSheet/ActionSheet- Bottom modal sheetsDisplay Components (7):
Avatar/AvatarGroup- User avatars with stackingBadge/StatusBadge- Labels and status indicatorsEmptyState/ErrorState/NoResults- Empty state displaysTooltip/InfoTip- Contextual information popupsCode Components (2):
DiffViewer- Code diff display with syntax highlightingFileTree- File/folder hierarchy navigationDesign Compliance
All components follow ThumbCode's brand guidelines:
Technical Details
Closes #13
Test Plan
pnpm tsc --noEmit)pnpm lint) - 1 complexity warning onlynpx jest)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
✏️ Tip: You can customize this high-level summary in your review settings.