feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 4 of 7)#4871
feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 4 of 7)#4871justincrich wants to merge 28 commits into
Conversation
📝 WalkthroughWalkthroughAdds on-device Storybook to the mobile app, gates boot via env flag, updates Metro, adds fonts/splash handling, and introduces numerous React Native UI components with corresponding stories and barrel exports. ChangesStorybook integration and UI components
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
|
Ready to review this PR? Stage has broken it down into 10 individual chapters for you: Chapters generated by Stage for commit a98b606 on May 26, 2026 9:02pm UTC. |
There was a problem hiding this comment.
2 issues found across 148 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mobile/app/_layout.tsx">
<violation number="1" location="apps/mobile/app/_layout.tsx:34">
P1: Font load errors are ignored, which can leave the app permanently stuck on the splash screen when fonts fail to load.</violation>
</file>
<file name="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx">
<violation number="1" location="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx:82">
P2: `detailed` mode claims internal scrolling but uses plain `Text` — content exceeding `max-h-32` is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.
Re-trigger cubic
| @@ -0,0 +1,133 @@ | |||
| import { Check, type LucideIcon, Target, X } from "lucide-react-native"; | |||
There was a problem hiding this comment.
P2: detailed mode claims internal scrolling but uses plain Text — content exceeding max-h-32 is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx, line 82:
<comment>`detailed` mode claims internal scrolling but uses plain `Text` — content exceeding `max-h-32` is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.</comment>
<file context>
@@ -0,0 +1,133 @@
+ className,
+ ...props
+}: PendingApprovalCardProps) {
+ const cfg = STATE[state];
+ const displayTitle = cfg.titleOverride ?? title;
+ const isResolving = state === "resolving";
</file context>
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (5)
apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx (1)
84-91: 💤 Low valueConsider using
accessibilityRole="status"instead of"alert".The status segment uses
accessibilityRole="alert", but"alert"is semantically intended for important, time-sensitive information requiring immediate user attention. This status indicator (streaming, thinking, completed, etc.) is informational rather than critical. React Native supports"status"which better represents ongoing state information that updates dynamically.♿ Proposed accessibility refinement
{showStatus ? ( <View - accessibilityRole="alert" + accessibilityRole="status" accessibilityLiveRegion="polite"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx` around lines 84 - 91, The accessibilityRole on the status container is currently set to "alert" but should be "status" because this component conveys non-critical, dynamic state; update the View in AssistantMessageHead (the element with accessibilityRole and accessibilityLiveRegion) to use accessibilityRole="status" (keep accessibilityLiveRegion="polite" and the existing className/variant logic unchanged) so screen readers treat it as ongoing informational status instead of an urgent alert.apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx (1)
86-89: ⚡ Quick winAvoid opacity-based state styling for state signaling.
Line 86 and Line 88 use opacity for interaction/disabled state, which conflicts with the semantic-color state model documented for this control. Prefer semantic bg/border/text state classes instead.
♻️ Proposed adjustment
- "flex-row items-center gap-1.5 h-7 px-3 rounded-full border border-border bg-card active:opacity-70", + "flex-row items-center gap-1.5 h-7 px-3 rounded-full border border-border bg-card active:bg-accent", isOpen && "bg-accent", - disabled && "opacity-50", + disabled && "bg-muted border-muted", className,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx` around lines 86 - 89, The button currently uses opacity classes for interaction/disabled states in ComposerSettingsButton (the JSX that composes the className array referencing isOpen, disabled, and className), which conflicts with the semantic-color model; replace opacity-based classes (active:opacity-70 and opacity-50) with semantic state classes instead (for example: use an active state background/border like active:bg-accent/active:border-accent or active:bg-accent-600 and a disabled semantic treatment such as bg-muted/border-muted/text-muted or disabled:bg-muted/disabled:border-muted), and ensure the isOpen branch still applies the intended accent background (bg-accent) while removing opacity manipulations so the control uses semantic bg/border/text classes for both active and disabled states.apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx (1)
32-45: ⚡ Quick winConsider merging className instead of allowing override.
The
ghostvariant appliesbg-transparent border-borderstyling (line 32-33), but because{...props}is spread after the explicitclassNameprop, anyclassNamepassed by the caller will override the ghost styling entirely. This could break the ghost variant's visual appearance.Consider using
cn()to merge the ghost styles with any caller-provided className:className={cn( variant === "ghost" && "bg-transparent border-border", props.className )}Then remove
classNamefrom the spread to avoid duplication.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx` around lines 32 - 45, The ghost variant's className on SuggestedAnswerPill is being overridden by the spread {...props}; update the JSX to merge the ghost styles with any caller className using the project's cn utility and pass that merged value to Pill (e.g., compute className = cn(variant === "ghost" && "bg-transparent border-border", props.className)) and then remove className from the {...props} spread so callers cannot fully override the ghost styling while still allowing additional classes.apps/mobile/components/ToastBase/ToastBase.tsx (1)
136-136: ⚡ Quick winConsider truncating long body text in accessibilityLabel.
When
bodyis used as the fallbackaccessibilityLabel, verbose toast messages may create overly long screen reader announcements. Consider truncating or summarizing long body text for better accessibility.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/ToastBase/ToastBase.tsx` at line 136, The accessibilityLabel prop currently falls back to title ?? body which can pass long verbose strings to screen readers; update the fallback logic in ToastBase (the accessibilityLabel assignment) to truncate or summarize body when used as the fallback (e.g., limit to a reasonable character count or words and append an ellipsis) so that if title is missing the computed accessibilityLabel uses a shortened version of body rather than the full text. Ensure the truncation function is applied only when using body as the fallback and keep title unchanged when present.apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx (1)
40-40: ⚡ Quick winConsider defensive handling for empty organization name.
If
activeOrgNameis an empty string,charAt(0)returns an empty string, resulting in an empty avatar fallback. While Storybook args default to "Superset", adding a fallback would make the component more robust.🛡️ Suggested defensive approach
-const orgInitial = activeOrgName.charAt(0).toUpperCase(); +const orgInitial = activeOrgName.charAt(0).toUpperCase() || '?';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/screens/`(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx at line 40, The code computes orgInitial from activeOrgName using activeOrgName.charAt(0), which yields an empty string for an empty name; update the logic around orgInitial in OrgDropdown.stories.tsx to defensively handle empty or falsy activeOrgName (e.g., check activeOrgName truthiness or length and fall back to a default character like "?" or the first char of a known default) so orgInitial is always a single visible character when rendering the avatar.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx`:
- Around line 80-114: The three ActivityIndicator instances inside
ApprovalFooter (the ones shown in the decline/approve/always Button render
branches controlled by resolving === "decline"/"approve"/"always") are using
className="text-..." which doesn't reliably set spinner color; replace className
with the ActivityIndicator prop color or colorClassName and supply the proper
color tokens (e.g., white or an accent/foreground token) for each case, removing
the text-* className so the spinner renders with the intended color.
In `@apps/mobile/components/CodeBlock/CodeBlock.tsx`:
- Around line 42-46: The handleCopy function schedules setCopied(false) with
setTimeout but never clears the timer, risking setState on an unmounted
component; store the timeout id (e.g., in a ref like copyTimeoutRef) when
calling setTimeout inside handleCopy, clear any existing timeout before creating
a new one, and add a useEffect cleanup that clears copyTimeoutRef.current on
unmount to prevent calling setCopied after the component is gone.
In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx`:
- Around line 87-105: The current unmount logic in PendingActionPill relies on
reading Reanimated shared values (opacity.value) which don't trigger React
renders, so the component never re-checks the `if (!visible && opacity.value ===
0) return null;` after the fade completes; fix by switching to a React
state-driven unmount or Reanimated entering/exiting: replace the manual
shared-value + useEffect flow (opacity, translateY, withTiming) with
Reanimated's layout/entering/exiting animations on the Animated.View (or use
withTiming's callback with runOnJS to call a local state setter such as
setIsMounted) so that when the animation finishes you call runOnJS(() =>
setIsMounted(false)) and use that isMounted state (instead of reading
opacity.value) to return null; update the render guard to check the React state
(isMounted && visible) and remove the shared-value read in the render path.
In `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx`:
- Around line 81-87: In SlashCommandOption, ensure the Pressable's
accessibilityState.disabled mirrors actual interactivity by using the combined
condition (disabled || isLoading) instead of the current disabled ?? false;
update accessibilityState.disabled to reflect both props so screen readers see
the item as disabled when isLoading is true (keep accessibilityState.busy mapped
to isLoading and selected mapped to isHighlighted).
In `@apps/mobile/components/ToastBase/ToastBase.tsx`:
- Line 149: The ActivityIndicator usage in ToastBase (ActivityIndicator element)
incorrectly passes a Tailwind-derived CSS class via the className prop
(mapping.iconColor); replace this by resolving the actual color value/token from
the Tailwind class (or a theme mapping) and pass it to the ActivityIndicator
using its color prop (e.g., set color={resolvedColor}) instead of className,
ensuring mapping.iconColor is converted to a valid color string before use.
In `@apps/mobile/components/ToolCallCard/ToolCallCard.tsx`:
- Around line 121-124: In ToolCallCard replace the unsupported className prop on
the ActivityIndicator: remove className="text-state-live-fg" and pass the
spinner color via the color prop instead (e.g.,
color={theme.colors['state-live-fg']} or a hex/string literal); ensure you
obtain the theme color (via existing useTheme / styles / constants) or hardcode
the hex for state-live-fg so the ActivityIndicator renders the intended color.
In `@apps/mobile/design/manifest.json`:
- Line 90: The manifest's molecule build-plan entries currently have "status":
"deferred" for some molecules while other entries and the scope notes indicate
Wave 2 is complete; update the offending "status" fields (the molecule
build-plan entries that are out of sync) to "passed" so the molecule statuses
are consistent with the completed gates and the other entries in the manifest.
---
Nitpick comments:
In `@apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx`:
- Around line 84-91: The accessibilityRole on the status container is currently
set to "alert" but should be "status" because this component conveys
non-critical, dynamic state; update the View in AssistantMessageHead (the
element with accessibilityRole and accessibilityLiveRegion) to use
accessibilityRole="status" (keep accessibilityLiveRegion="polite" and the
existing className/variant logic unchanged) so screen readers treat it as
ongoing informational status instead of an urgent alert.
In `@apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx`:
- Around line 86-89: The button currently uses opacity classes for
interaction/disabled states in ComposerSettingsButton (the JSX that composes the
className array referencing isOpen, disabled, and className), which conflicts
with the semantic-color model; replace opacity-based classes (active:opacity-70
and opacity-50) with semantic state classes instead (for example: use an active
state background/border like active:bg-accent/active:border-accent or
active:bg-accent-600 and a disabled semantic treatment such as
bg-muted/border-muted/text-muted or disabled:bg-muted/disabled:border-muted),
and ensure the isOpen branch still applies the intended accent background
(bg-accent) while removing opacity manipulations so the control uses semantic
bg/border/text classes for both active and disabled states.
In `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx`:
- Around line 32-45: The ghost variant's className on SuggestedAnswerPill is
being overridden by the spread {...props}; update the JSX to merge the ghost
styles with any caller className using the project's cn utility and pass that
merged value to Pill (e.g., compute className = cn(variant === "ghost" &&
"bg-transparent border-border", props.className)) and then remove className from
the {...props} spread so callers cannot fully override the ghost styling while
still allowing additional classes.
In `@apps/mobile/components/ToastBase/ToastBase.tsx`:
- Line 136: The accessibilityLabel prop currently falls back to title ?? body
which can pass long verbose strings to screen readers; update the fallback logic
in ToastBase (the accessibilityLabel assignment) to truncate or summarize body
when used as the fallback (e.g., limit to a reasonable character count or words
and append an ellipsis) so that if title is missing the computed
accessibilityLabel uses a shortened version of body rather than the full text.
Ensure the truncation function is applied only when using body as the fallback
and keep title unchanged when present.
In
`@apps/mobile/screens/`(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx:
- Line 40: The code computes orgInitial from activeOrgName using
activeOrgName.charAt(0), which yields an empty string for an empty name; update
the logic around orgInitial in OrgDropdown.stories.tsx to defensively handle
empty or falsy activeOrgName (e.g., check activeOrgName truthiness or length and
fall back to a default character like "?" or the first char of a known default)
so orgInitial is always a single visible character when rendering the avatar.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1412f4a1-6863-49ed-8710-313650c8235d
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (147)
apps/mobile/.rnstorybook/.gitignoreapps/mobile/.rnstorybook/index.tsxapps/mobile/.rnstorybook/main.jsapps/mobile/.rnstorybook/preview.tsxapps/mobile/.rnstorybook/stories/DesignSystem/Colors.stories.tsxapps/mobile/.rnstorybook/stories/DesignSystem/Icons.stories.tsxapps/mobile/.rnstorybook/stories/DesignSystem/Spacing.stories.tsxapps/mobile/.rnstorybook/stories/DesignSystem/Typography.stories.tsxapps/mobile/app/_layout.tsxapps/mobile/components/AppHeader/AppHeader.stories.tsxapps/mobile/components/AppHeader/AppHeader.tsxapps/mobile/components/AppHeader/index.tsapps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsxapps/mobile/components/ApprovalFooter/ApprovalFooter.tsxapps/mobile/components/ApprovalFooter/index.tsapps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsxapps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsxapps/mobile/components/AssistantMessageHead/index.tsapps/mobile/components/Banner/Banner.stories.tsxapps/mobile/components/Banner/Banner.tsxapps/mobile/components/Banner/index.tsapps/mobile/components/CodeBlock/CodeBlock.stories.tsxapps/mobile/components/CodeBlock/CodeBlock.tsxapps/mobile/components/CodeBlock/index.tsapps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsxapps/mobile/components/CollapsedBlock/CollapsedBlock.tsxapps/mobile/components/CollapsedBlock/index.tsapps/mobile/components/ComposerRow/ComposerRow.stories.tsxapps/mobile/components/ComposerRow/ComposerRow.tsxapps/mobile/components/ComposerRow/index.tsapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsxapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsxapps/mobile/components/ComposerSettingsButton/index.tsapps/mobile/components/FabBase/FabBase.stories.tsxapps/mobile/components/FabBase/FabBase.tsxapps/mobile/components/FabBase/index.tsapps/mobile/components/HelloWorld/HelloWorld.stories.tsxapps/mobile/components/HelloWorld/HelloWorld.tsxapps/mobile/components/HelloWorld/index.tsapps/mobile/components/HitTargetWrapper/HitTargetWrapper.stories.tsxapps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsxapps/mobile/components/HitTargetWrapper/index.tsapps/mobile/components/IconButton/IconButton.stories.tsxapps/mobile/components/IconButton/IconButton.tsxapps/mobile/components/IconButton/index.tsapps/mobile/components/ModalHeader/ModalHeader.stories.tsxapps/mobile/components/ModalHeader/ModalHeader.tsxapps/mobile/components/ModalHeader/index.tsapps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsxapps/mobile/components/ModelPickerOption/ModelPickerOption.tsxapps/mobile/components/ModelPickerOption/index.tsapps/mobile/components/PendingActionPill/PendingActionPill.stories.tsxapps/mobile/components/PendingActionPill/PendingActionPill.tsxapps/mobile/components/PendingActionPill/index.tsapps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsxapps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsxapps/mobile/components/PendingApprovalCard/index.tsapps/mobile/components/PickerTrigger/PickerTrigger.stories.tsxapps/mobile/components/PickerTrigger/PickerTrigger.tsxapps/mobile/components/PickerTrigger/index.tsapps/mobile/components/Pill/Pill.stories.tsxapps/mobile/components/Pill/Pill.tsxapps/mobile/components/Pill/index.tsapps/mobile/components/ProgressDots/ProgressDots.stories.tsxapps/mobile/components/ProgressDots/ProgressDots.tsxapps/mobile/components/ProgressDots/index.tsapps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsxapps/mobile/components/ScrollBackButton/ScrollBackButton.tsxapps/mobile/components/ScrollBackButton/index.tsapps/mobile/components/ScrollFade/ScrollFade.stories.tsxapps/mobile/components/ScrollFade/ScrollFade.tsxapps/mobile/components/ScrollFade/index.tsapps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsxapps/mobile/components/SlashCommandOption/SlashCommandOption.tsxapps/mobile/components/SlashCommandOption/index.tsapps/mobile/components/StatusDot/StatusDot.stories.tsxapps/mobile/components/StatusDot/StatusDot.tsxapps/mobile/components/StatusDot/index.tsapps/mobile/components/StreamingCursor/StreamingCursor.stories.tsxapps/mobile/components/StreamingCursor/StreamingCursor.tsxapps/mobile/components/StreamingCursor/index.tsapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsxapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsxapps/mobile/components/SuggestedAnswerPill/index.tsapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsxapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsxapps/mobile/components/ThinkingLevelOption/index.tsapps/mobile/components/ToastBase/ToastBase.stories.tsxapps/mobile/components/ToastBase/ToastBase.tsxapps/mobile/components/ToastBase/index.tsapps/mobile/components/ToolCallCard/ToolCallCard.stories.tsxapps/mobile/components/ToolCallCard/ToolCallCard.tsxapps/mobile/components/ToolCallCard/index.tsapps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsxapps/mobile/components/ToolStatusRule/ToolStatusRule.tsxapps/mobile/components/ToolStatusRule/index.tsapps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsxapps/mobile/components/UserMessageBubble/UserMessageBubble.tsxapps/mobile/components/UserMessageBubble/index.tsapps/mobile/components/ui/AUDIT.mdapps/mobile/components/ui/accordion.stories.tsxapps/mobile/components/ui/alert-dialog.stories.tsxapps/mobile/components/ui/alert.stories.tsxapps/mobile/components/ui/aspect-ratio.stories.tsxapps/mobile/components/ui/avatar.stories.tsxapps/mobile/components/ui/badge.stories.tsxapps/mobile/components/ui/button.stories.tsxapps/mobile/components/ui/card.stories.tsxapps/mobile/components/ui/checkbox.stories.tsxapps/mobile/components/ui/collapsible.stories.tsxapps/mobile/components/ui/context-menu.stories.tsxapps/mobile/components/ui/dialog.stories.tsxapps/mobile/components/ui/dropdown-menu.stories.tsxapps/mobile/components/ui/hover-card.stories.tsxapps/mobile/components/ui/icon.stories.tsxapps/mobile/components/ui/input.stories.tsxapps/mobile/components/ui/label.stories.tsxapps/mobile/components/ui/menubar.stories.tsxapps/mobile/components/ui/popover.stories.tsxapps/mobile/components/ui/progress.stories.tsxapps/mobile/components/ui/radio-group.stories.tsxapps/mobile/components/ui/select.stories.tsxapps/mobile/components/ui/separator.stories.tsxapps/mobile/components/ui/skeleton.stories.tsxapps/mobile/components/ui/switch.stories.tsxapps/mobile/components/ui/tabs.stories.tsxapps/mobile/components/ui/text.stories.tsxapps/mobile/components/ui/textarea.stories.tsxapps/mobile/components/ui/toggle-group.stories.tsxapps/mobile/components/ui/toggle.stories.tsxapps/mobile/components/ui/tooltip.stories.tsxapps/mobile/design/manifest.jsonapps/mobile/global.cssapps/mobile/lib/theme.tsapps/mobile/metro.config.jsapps/mobile/package.jsonapps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsxapps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsxapps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.stories.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsxapps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsxapps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsxapps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsxapps/mobile/screens/AUDIT.md
| {resolving === "decline" ? ( | ||
| <ActivityIndicator size="small" className="text-white" /> | ||
| ) : ( | ||
| <Text>Decline</Text> | ||
| )} | ||
| </Button> | ||
| <Button | ||
| variant="default" | ||
| size="default" | ||
| className="flex-1 h-touch-min" | ||
| onPress={onApprove} | ||
| accessibilityLabel="Approve tool action" | ||
| > | ||
| {resolving === "approve" ? ( | ||
| <ActivityIndicator | ||
| size="small" | ||
| className="text-primary-foreground" | ||
| /> | ||
| ) : ( | ||
| <Text>Approve</Text> | ||
| )} | ||
| </Button> | ||
| <Button | ||
| variant="ghost" | ||
| size="default" | ||
| className="flex-1 h-touch-min" | ||
| onPress={onAlways} | ||
| accessibilityLabel="Always allow this tool category" | ||
| > | ||
| {resolving === "always" ? ( | ||
| <ActivityIndicator size="small" className="text-foreground" /> | ||
| ) : ( | ||
| <Text>Always</Text> | ||
| )} | ||
| </Button> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify ActivityIndicator API in React Native
rg -n "import.*ActivityIndicator" apps/mobile/components -A5 | head -20Repository: superset-sh/superset
Length of output: 1925
🌐 Web query:
Does React Native ActivityIndicator support className prop for styling or does it use a color prop?
💡 Result:
React Native’s built-in ActivityIndicator does not support a className prop for styling. Instead, it exposes a color prop to control the spinner’s foreground color, and it can accept normal View-style props via the style prop. The official React Native docs list supported props for ActivityIndicator and include a color prop (“The foreground color of the spinner”) but no className prop. [1] So, for React Native ActivityIndicator, use: - color={...} to change spinner color [1] - style={...} (inherited View props) for other styling needs like layout/positioning [1] If you see className mentioned, that’s coming from a non-core system (e.g., a library that adds className support such as NativeWind/NativeWind bindings), not from React Native’s ActivityIndicator itself. [2]
Citations:
- 1: https://reactnative.dev/docs/activityindicator
- 2: https://docs.uniwind.dev/components/activity-indicator
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,180p' apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | nl -ba | sed -n '60,140p'Repository: superset-sh/superset
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact section containing the three ActivityIndicator usages
nl -ba apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | sed -n '60,140p'Repository: superset-sh/superset
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact section containing the three ActivityIndicator usages
nl -ba apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | sed -n '60,140p'Repository: superset-sh/superset
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | sed -n '70,130p'Repository: superset-sh/superset
Length of output: 1599
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find how className is supported for RN components (NativeWind / Tailwind RN)
rg -n --hidden -S "nativewind|tailwind-rn|nativewindcss|unwind|NativeWind" apps/mobile | head -50 || true
# 2) Find other ActivityIndicator usages in mobile to see if color prop is used
rg -n "ActivityIndicator" apps/mobile/components -SRepository: superset-sh/superset
Length of output: 2769
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show ToastBase ActivityIndicator usage around the reported line
sed -n '120,190p' apps/mobile/components/ToastBase/ToastBase.tsx
# Show IconButton ActivityIndicator usage around the reported lines
sed -n '90,170p' apps/mobile/components/IconButton/IconButton.tsx
# Show FabBase ActivityIndicator usage around the reported lines
sed -n '140,220p' apps/mobile/components/FabBase/FabBase.tsx
# Show ToolCallCard ActivityIndicator usage around the reported lines
sed -n '90,160p' apps/mobile/components/ToolCallCard/ToolCallCard.tsxRepository: superset-sh/superset
Length of output: 5986
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Any ActivityIndicator with explicit `color` prop in the mobile app
rg -n "ActivityIndicator[^>]*color=" apps/mobile | head -50 || true
# Check ActivityIndicator className usage pattern
rg -n "ActivityIndicator[^>]*className=" apps/mobile | head -50
# Look for NativeWind / Tailwind-RN config in the mobile app
rg -n "nativewind|tailwind-rn|babel-plugin|tw=" apps/mobile . | head -100 || true
# Look for Tailwind / NativeWind config files at repo root (common locations)
ls -la | head -50Repository: superset-sh/superset
Length of output: 19889
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --hidden -S "nativewind|tailwind-rn|unwind|nativewindcss" . | head -100Repository: superset-sh/superset
Length of output: 591
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for any known RN Tailwind/className adapters or config files
rg -n --hidden -S "uniwind|NativeWindStyleSheet|nativewind|tailwindcss-react-native|react-native-unwind|unwind" apps/mobile packages plugins scripts . | head -100
# Also search for Tailwind config usage
rg -n --hidden -S "tailwind.config|twConfig|tw=" apps/mobile . | head -100
# Find how Text/Button components implement className (to infer whether RN className transformer exists)
rg -n "function .*\\b(className)\\b" apps/mobile/components -S | head -50Repository: superset-sh/superset
Length of output: 26230
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "ActivityIndicator" apps/mobile/uniwind-types.d.ts apps/mobile/uniwind-env.d.ts | head -50Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any ActivityIndicator-related typing overrides
rg -n "ActivityIndicator" apps/mobile/uniwind-types.d.ts apps/mobile/uniwind-env.d.ts || true
# Search for whether uniwind adds/extends className on RN component types
rg -n "className" apps/mobile/uniwind-types.d.ts | head -50Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
#!/bin/bash
set -euo pipefail
wc -l apps/mobile/uniwind-types.d.ts
head -120 apps/mobile/uniwind-types.d.tsRepository: superset-sh/superset
Length of output: 346
🌐 Web query:
uniwind ActivityIndicator className color prop text-*
💡 Result:
In Uniwind (React Native), for ActivityIndicator: 1) className controls style, so Tailwind text-* classes (e.g., text-red-500) apply to styles mapped from className (and not to the spinner “foreground” color). 2) The spinner’s foreground color is set via the color prop, which in Uniwind must be provided through colorClassName with an accent-* prefix. Correct (Uniwind): Wrong (Uniwind): (because color is a non-style color prop, so using className with text-* does not set ActivityIndicator’s color; Uniwind requires an accent-* prefixed {propName}ClassName such as colorClassName for the color prop) [1][2][3]. Uniwind docs explicitly state that for “non-style props (like color)”, you must use the accent- prefix via colorClassName, and that since 1.6.2 passing a className without accent- to a color prop logs a warning [1]. The Uniwind SKILL notes reiterate that for ActivityIndicator, colorClassName maps to the color prop and requires accent-* [2], and that className maps to style [2].
Citations:
- 1: https://docs.uniwind.dev/components/activity-indicator
- 2: https://github.com/uni-stack/uniwind/blob/main/skills/uniwind/SKILL.md
- 3: https://docs.uniwind.dev/class-names
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "ActivityIndicator[^>]*colorClassName" apps/mobile | head -50Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect ToastBase variant mapping where ActivityIndicator className is sourced
rg -n "iconColor|variantMapping" apps/mobile/components/ToastBase/ToastBase.tsx
sed -n '1,220p' apps/mobile/components/ToastBase/ToastBase.tsx
# Inspect IconButton iconColorByVariant where ActivityIndicator className is sourced
rg -n "iconColorByVariant" apps/mobile/components/IconButton/IconButton.tsx
sed -n '1,220p' apps/mobile/components/IconButton/IconButton.tsx
# Inspect FabBase iconColorByVariant where ActivityIndicator className is sourced
rg -n "iconColorByVariant" apps/mobile/components/FabBase/FabBase.tsx
sed -n '1,260p' apps/mobile/components/FabBase/FabBase.tsxRepository: superset-sh/superset
Length of output: 14547
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find any usage of uniwind colorClassName / accent-*
rg -n "colorClassName\\s*=" apps/mobile | head -50 || true
rg -n "accent-" apps/mobile/components apps/mobile/screens apps/mobile/hooks | head -50 || trueRepository: superset-sh/superset
Length of output: 5110
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -ლა apps/mobile | head -50
ls -ლა apps/mobile | rg "tailwind" || true
cat apps/mobile/tailwind.config.js 2>/dev/null || true
cat apps/mobile/tailwind.config.ts 2>/dev/null || true
fd tailwind.config -t f apps/mobile -a 2>/dev/null || trueRepository: superset-sh/superset
Length of output: 128
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ls -la apps/mobile | head -50
fd tailwind.config -t f apps/mobile || true
fd tailwind.config -t f . | head -20
cat apps/mobile/tailwind.config.js 2>/dev/null || true
cat apps/mobile/tailwind.config.ts 2>/dev/null || trueRepository: superset-sh/superset
Length of output: 1620
Fix ActivityIndicator spinner color in ApprovalFooter
ActivityIndicator’s spinner foreground color is controlled via color/Uniwind’s colorClassName; the current className="text-..." on the spinners won’t reliably apply the intended white/foreground colors.
Update the three ActivityIndicator usages (decline/approve/always) to use color or colorClassName (accent-* tokens) instead of className.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx` around lines 80 -
114, The three ActivityIndicator instances inside ApprovalFooter (the ones shown
in the decline/approve/always Button render branches controlled by resolving ===
"decline"/"approve"/"always") are using className="text-..." which doesn't
reliably set spinner color; replace className with the ActivityIndicator prop
color or colorClassName and supply the proper color tokens (e.g., white or an
accent/foreground token) for each case, removing the text-* className so the
spinner renders with the intended color.
| const handleCopy = () => { | ||
| onCopy?.(code); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1500); | ||
| }; |
There was a problem hiding this comment.
Clean up setTimeout to prevent setState on unmounted component.
The setTimeout at line 45 is not cleaned up. If the component unmounts before 1500ms elapses, React will attempt to call setCopied(false) on an unmounted component, causing warnings and a potential memory leak.
🔧 Proposed fix using useEffect cleanup
-export function CodeBlock({
+import { useEffect, useRef } from "react";
+
+export function CodeBlock({
code,
language,
onCopy,
overflow,
bare,
className,
...props
}: CodeBlockProps) {
const [copied, setCopied] = useState(false);
+ const timeoutRef = useRef<NodeJS.Timeout>();
const handleCopy = () => {
onCopy?.(code);
setCopied(true);
- setTimeout(() => setCopied(false), 1500);
+ timeoutRef.current = setTimeout(() => setCopied(false), 1500);
};
+
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ }, []);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/CodeBlock/CodeBlock.tsx` around lines 42 - 46, The
handleCopy function schedules setCopied(false) with setTimeout but never clears
the timer, risking setState on an unmounted component; store the timeout id
(e.g., in a ref like copyTimeoutRef) when calling setTimeout inside handleCopy,
clear any existing timeout before creating a new one, and add a useEffect
cleanup that clears copyTimeoutRef.current on unmount to prevent calling
setCopied after the component is gone.
| const opacity = useSharedValue(visible ? 1 : 0); | ||
| const translateY = useSharedValue(visible ? 0 : 8); | ||
|
|
||
| useEffect(() => { | ||
| opacity.value = withTiming(visible ? 1 : 0, { duration: 200 }); | ||
| translateY.value = withTiming(visible ? 0 : 8, { duration: 200 }); | ||
| }, [opacity, translateY, visible]); | ||
|
|
||
| const animatedStyle = useAnimatedStyle( | ||
| () => ({ | ||
| opacity: opacity.value, | ||
| transform: [{ translateY: translateY.value }], | ||
| }), | ||
| [opacity, translateY], | ||
| ); | ||
|
|
||
| if (!visible && opacity.value === 0) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Shared value reads don't trigger re-renders; unmount condition won't work as intended.
Lines 103-105 attempt to return null when !visible && opacity.value === 0, but this has a critical flaw:
- When
visiblebecomesfalse, the useEffect starts animatingopacity.valuefrom 1 → 0 over 200ms - However, shared values don't trigger React re-renders when they change
- The condition is checked during render, but after the animation completes there's no re-render to re-check it
- Result: The component continues to render (just invisible) even after the fade-out animation finishes
🔧 Recommended fix using Reanimated entering/exiting
Replace manual shared value + useEffect approach with Reanimated's built-in entering/exiting animations, which handle unmounting automatically:
import Animated, {
- useAnimatedStyle,
- useSharedValue,
- withTiming,
+ FadeIn,
+ FadeOut,
} from "react-native-reanimated";
export function PendingActionPill({
// ... props
}: PendingActionPillProps) {
- const opacity = useSharedValue(visible ? 1 : 0);
- const translateY = useSharedValue(visible ? 0 : 8);
-
- useEffect(() => {
- opacity.value = withTiming(visible ? 1 : 0, { duration: 200 });
- translateY.value = withTiming(visible ? 0 : 8, { duration: 200 });
- }, [opacity, translateY, visible]);
-
- const animatedStyle = useAnimatedStyle(
- () => ({
- opacity: opacity.value,
- transform: [{ translateY: translateY.value }],
- }),
- [opacity, translateY],
- );
-
- if (!visible && opacity.value === 0) {
+ if (!visible) {
return null;
}
return (
- <Animated.View style={animatedStyle}>
+ <Animated.View
+ entering={FadeIn.duration(200)}
+ exiting={FadeOut.duration(200)}
+ >Alternatively, if you need the translateY animation, use a finishing callback with runOnJS to update local state and trigger unmount.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx` around lines
87 - 105, The current unmount logic in PendingActionPill relies on reading
Reanimated shared values (opacity.value) which don't trigger React renders, so
the component never re-checks the `if (!visible && opacity.value === 0) return
null;` after the fade completes; fix by switching to a React state-driven
unmount or Reanimated entering/exiting: replace the manual shared-value +
useEffect flow (opacity, translateY, withTiming) with Reanimated's
layout/entering/exiting animations on the Animated.View (or use withTiming's
callback with runOnJS to call a local state setter such as setIsMounted) so that
when the animation finishes you call runOnJS(() => setIsMounted(false)) and use
that isMounted state (instead of reading opacity.value) to return null; update
the render guard to check the React state (isMounted && visible) and remove the
shared-value read in the render path.
| accessibilityState={{ | ||
| selected: isHighlighted ?? false, | ||
| disabled: disabled ?? false, | ||
| busy: isLoading ?? false, | ||
| }} | ||
| disabled={disabled || isLoading} | ||
| className={cn( |
There was a problem hiding this comment.
Synchronize accessibility disabled state with actual interactivity.
Pressable becomes non-interactive when isLoading is true, but accessibilityState.disabled can still announce it as enabled.
Suggested fix
export function SlashCommandOption({
name,
description,
source = "builtin",
icon,
isHighlighted,
isLoading,
className,
disabled,
...props
}: SlashCommandOptionProps) {
+ const isDisabled = Boolean(disabled || isLoading);
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={`${name} — ${description}`}
accessibilityState={{
selected: isHighlighted ?? false,
- disabled: disabled ?? false,
+ disabled: isDisabled,
busy: isLoading ?? false,
}}
- disabled={disabled || isLoading}
+ disabled={isDisabled}
className={cn(
slashCommandOptionVariants({ isHighlighted, isLoading }),
disabled && "opacity-40",
className,
)}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx` around
lines 81 - 87, In SlashCommandOption, ensure the Pressable's
accessibilityState.disabled mirrors actual interactivity by using the combined
condition (disabled || isLoading) instead of the current disabled ?? false;
update accessibilityState.disabled to reflect both props so screen readers see
the item as disabled when isLoading is true (keep accessibilityState.busy mapped
to isLoading and selected mapped to isHighlighted).
| > | ||
| <View className={cn(isStacked ? "pt-0.5" : "")}> | ||
| {variant === "loading" ? ( | ||
| <ActivityIndicator size="small" className={mapping.iconColor} /> |
There was a problem hiding this comment.
ActivityIndicator does not support className prop.
React Native's ActivityIndicator does not accept a className prop for styling. The color should be set via the color prop instead. This line will not apply the intended color styling.
🐛 Proposed fix
- {variant === "loading" ? (
- <ActivityIndicator size="small" className={mapping.iconColor} />
- ) : (
+ {variant === "loading" ? (
+ <ActivityIndicator size="small" color={mapping.iconColor} />
+ ) : (Note: You'll need to resolve the actual color value from the Tailwind class. Consider extracting color values or using a direct color string/token.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ToastBase/ToastBase.tsx` at line 149, The
ActivityIndicator usage in ToastBase (ActivityIndicator element) incorrectly
passes a Tailwind-derived CSS class via the className prop (mapping.iconColor);
replace this by resolving the actual color value/token from the Tailwind class
(or a theme mapping) and pass it to the ActivityIndicator using its color prop
(e.g., set color={resolvedColor}) instead of className, ensuring
mapping.iconColor is converted to a valid color string before use.
| <ActivityIndicator | ||
| size="small" | ||
| className="text-state-live-fg" | ||
| /> |
There was a problem hiding this comment.
ActivityIndicator does not support className prop.
React Native's ActivityIndicator does not accept a className prop. Use the color prop instead to set the spinner color. This will not apply the intended text-state-live-fg color.
🐛 Proposed fix
{cfg.showSpinner ? (
- <ActivityIndicator
- size="small"
- className="text-state-live-fg"
- />
+ <ActivityIndicator
+ size="small"
+ color="`#50a878`"
+ />
) : null}Note: Replace the hex color with the appropriate theme color value for state-live-fg.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ToolCallCard/ToolCallCard.tsx` around lines 121 - 124,
In ToolCallCard replace the unsupported className prop on the ActivityIndicator:
remove className="text-state-live-fg" and pass the spinner color via the color
prop instead (e.g., color={theme.colors['state-live-fg']} or a hex/string
literal); ensure you obtain the theme color (via existing useTheme / styles /
constants) or hardcode the hex for state-live-fg so the ActivityIndicator
renders the intended color.
| ] | ||
| }, | ||
| "molecules": { | ||
| "status": "deferred", |
There was a problem hiding this comment.
Align molecule build-plan status with completed gates.
Line 90 and Line 142 still mark molecules as deferred, while Line 45 and Line 135 set molecules to passed and the scope notes describe Wave 2 as completed. This inconsistency can cause tooling to read stale progress state.
Also applies to: 142-142
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/design/manifest.json` at line 90, The manifest's molecule
build-plan entries currently have "status": "deferred" for some molecules while
other entries and the scope notes indicate Wave 2 is complete; update the
offending "status" fields (the molecule build-plan entries that are out of sync)
to "passed" so the molecule statuses are consistent with the completed gates and
the other entries in the manifest.
Cherry-picked from f4f2a687b (originally on the deleted chat-mobile-ui-elements
branch). Adds Storybook 9 native sandbox env-gated on EXPO_PUBLIC_STORYBOOK,
Design System token stories (Colors/Typography/Spacing/Icons reading existing
global.css tokens via className), and a HelloWorld reference component.
Manifest updated with structured `constraints` block:
- preserve_theme: lib/theme.ts, global.css, uniwind-env.d.ts, uniwind-types.d.ts
are canonical — scaffold and later phases must not overwrite.
- wireframes_are_reference_only: PRD ASCII wireframes describe structural
intent only; use frontend-design skill for high-fidelity during build.
Gates: discover/target/equip/scaffold = passed on mobile-ios and mobile-android.
Next: /pixel-perfect:build --platform mobile-ios (or mobile-android).
Adds storybook 9 + addon-ondevice-actions/controls + @storybook/react-native devDeps in apps/mobile to match the cherry-picked package.json. Also picks up apps/desktop 1.10.3 -> 1.11.0 from main.
…Phase 0) Execute the token migration described in plans/chat-mobile-plan/14-token-migration-audit.md. Path A: keep flat shadcn key names (--color-*) for rn-reusables CLI compatibility; swap values to desktop ember warm palette + add chat-domain extensions (state palette, domain tokens, fonts, touch-target spacing). Vendor react-native-reusables components in apps/mobile/components/ui/* are not touched — they read tokens at runtime and cascade automatically against the new values, per the "vendor libraries + style overrides only" rule. - apps/mobile/global.css: rewrite under Tailwind 4 @theme + uniwind @variant. Warm-neutral ramp (#151110 background / #201e1c card / #2a2827 secondary in dark; #ffffff / warm-tinted light grays in light). Ember accent #e07850 (hsl(17 69% 60%)) as --color-primary in both themes. Add state palette (live/warning/danger/success/neutral × fg/bg) and chat domain tokens (streaming-cursor, tool-rule). Pre-compute oklch literals as hsl for RN safety; omit color-mix() hover/pressed (rn-reusables handles interaction via opacity/scale). - apps/mobile/lib/theme.ts: mirror global.css key-for-key. NAV_THEME.primary now resolves to ember (was inverted-neutral); NAV_THEME.notification stays destructive per audit §4. Add stateXxx + streamingCursor / toolRule + fontBody / fontMono. - apps/mobile/app/_layout.tsx: wire Geist + Geist Mono via @expo-google-fonts/geist with SplashScreen.preventAutoHideAsync gate. Storybook + production both wait for fonts. - apps/mobile/.rnstorybook/stories/DesignSystem/Colors.stories.tsx: extend with State palette + Domain tokens sections. - apps/mobile/.rnstorybook/stories/DesignSystem/Typography.stories.tsx: add Font families section demonstrating all 4 Geist weights + Geist Mono weights via fontFamily prop. - apps/mobile/design/manifest.json: bump to v5.1.0. Vibe rewritten to ember. Narrow preserve_theme.paths to uniwind machinery only (global.css + lib/theme.ts no longer locked). Add vendor_components_immutable constraint (per the new rule). Add tokens_source pointer to designs/tokens/tokens.css. Add fonts tool spec to both platform entries. - apps/mobile/package.json + bun.lock: add @expo-google-fonts/geist 0.4.2, @expo-google-fonts/geist-mono 0.4.2, expo-font ~56, expo-splash-screen ~56. Verified: bun typecheck passes (exit 0), biome check passes on touched files.
Storybook v9 react-native does not auto-detect AsyncStorage when
`shouldPersistSelection: true` — it expects the consumer to pass a
`storage` adapter explicitly. Without one, the persistence layer attempts
to call `.getItem()` on `undefined` and throws:
TypeError: Cannot read property 'getItem' of undefined
This appears on first launch when Storybook tries to read the last-selected
story.
Fix: pass `{ getItem: AsyncStorage.getItem, setItem: AsyncStorage.setItem }`
from `@react-native-async-storage/async-storage` (already a dep). Storybook
now persists story selection across launches without erroring.
Storybook 9.x's `instrumenter` (transitively via @storybook/addon-ondevice-
controls + @storybook/addon-ondevice-actions) pulls in `tinyrainbow`, which
requires Node's built-in `tty` module. Metro cannot bundle it, surfacing as:
ERROR Unable to resolve module tty from
.../storybook@9.1.20/.../instrumenter/index.cjs
Fix: shim `tty` to an empty module via `resolver.resolveRequest`. Returning
`{ type: "empty" }` is Metro's built-in pattern for Node built-ins it does
not bundle (same trick used for `fs`, `path`, etc. in RN bundles).
Pre-existing infra issue surfaced during pixel-perfect Wave-1 atom
verification; unrelated to Wave-1 atom additions.
Several screen modules (OrganizationHeaderButton, AuthenticatedTabBar, TabBarAccessory, OrgDropdown, MoreMenuScreen, SettingsScreen, etc.) call `useRouter`/`useNavigation` hooks that require an active expo-router NavigationContainer. Storybook 9's `addon-ondevice-controls` eagerly evaluates each story's render function during `createPreparedStoryMapping`, which throws "Couldn't find an UnhandledLinkingContext context" outside a running navigator. The affected stories already comment-acknowledge they're "not renderable in Storybook isolation" — they exist mainly as documentation surfaces. Commenting out the `../screens/**/*.stories.?(ts|tsx|js|jsx)` glob in .rnstorybook/main.js stops Storybook from pulling them into the bundle. To restore screen stories later, uncomment the glob AND add a nav-mock decorator to preview.tsx that provides UnhandledLinkingContext + a stub useRouter (or wrap the story tree in a real NavigationContainer with mock state). Pre-existing infra issue surfaced during pixel-perfect Wave-1 atom verification; unrelated to Wave-1 atom additions.
Pre-existing screen modules (OrganizationHeaderButton, AuthenticatedTabBar, TabBarAccessory, OrgDropdown, MoreMenuScreen, SettingsScreen, etc.) call `useRouter`/`useNavigation` hooks. Storybook 9's `addon-ondevice-controls` eagerly evaluates each story's render path during `createPreparedStoryMapping`, throwing "Couldn't find an UnhandledLinkingContext context" outside a running NavigationContainer. Fix: wrap the preview decorator chain in `<NavigationContainer>` from `expo-router/react-navigation`. That sets up `UnhandledLinkingContext`, `LinkingContext`, `LocaleDirContext`, and the base navigation state — the same contexts expo-router's `<ExpoRoot>` provides in the real app. Using the expo-router sub-path (`expo-router/react-navigation`) instead of `@react-navigation/native` directly so we ride on apps/mobile's already- declared expo-router dep — avoids a phantom-dep on `@react-navigation/native` (which is only present transitively in the bun store). Reverts the prior commit `dedf3dd56` that hid screens stories from main.js. Screens stories now render correctly in Storybook isolation.
… prep Tried wrapping the storybook preview chain in `<NavigationContainer>` from `expo-router/react-navigation` (commit 0e805a1). Did not resolve the `UnhandledLinkingContext` error. Root cause: Storybook 9 RN's `loadStory` (called inside `createPreparedStoryMapping`) does eager module + render-fn evaluation BEFORE preview decorators apply. The screen placeholder stories transitively import `useTheme` → `lib/theme.ts` → `expo-router/react-navigation`, which during prep calls `useLinking` → reads the default `UnhandledLinkingContext` value's getter → throws "Couldn't find an UnhandledLinkingContext context." Decorators in preview.tsx wrap render-time only, not prep-time. So the NavigationContainer wrapper is structurally unable to fix this chain. Re-exclude `../screens/**/*.stories.?(ts|tsx)` (same as the earlier dedf3dd commit). Keep the NavigationContainer in preview.tsx as defense for any future story that DOES route through decorators and needs nav context (e.g. a future composer molecule that uses `<Link>`). Long-term restoration of screen stories requires decoupling them from `lib/theme.ts` (mirror the pattern used in `components/ScrollFade/ScrollFade.tsx`) or moving them under `expo-router/testing-library`'s `renderRouter`.
353396c to
43f6299
Compare
Unblock storybook RN prep for views that transitively import expo-router by providing a self-contained StorybookRouterProvider wrapping the preview-time linking contexts. - Add StorybookRouterProvider that wraps PreviewRouteContext + LinkingContext + UnhandledLinkingContext so views (including AskUserSheet from REMED-009) can render in Storybook without crashing on "Couldn't find an UnhandledLinkingContext context." - Update preview.tsx + index.tsx to wrap stories in StorybookRouterProvider; remove the prior "do not import nav modules" ban (now properly worked-around at runtime, not at module-load time). - metro.config.js teaches metro to resolve the new router/ subpath + storybook config files in app bundling. Follow-up to ae4494c (REMED-009 task commit).
a0edf5e to
7d64168
Compare
There was a problem hiding this comment.
2 issues found across 172 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx">
<violation number="1" location="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx:82">
P2: `detailed` mode claims internal scrolling but uses plain `Text` — content exceeding `max-h-32` is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.</violation>
</file>
<file name="apps/mobile/components/IconButton/IconButton.tsx">
<violation number="1" location="apps/mobile/components/IconButton/IconButton.tsx:110">
P2: accessibilityState can be overridden by caller-supplied props due to spread ordering</violation>
</file>
<file name="apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx">
<violation number="1" location="apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx:87">
P2: Disabled state is not propagated to the nested `RadioGroupItem`, so tapping the radio control itself can still select the option even when the outer row is disabled.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.
Re-trigger cubic
| return ( | ||
| <Button | ||
| accessibilityLabel={accessibilityLabel} | ||
| accessibilityState={{ disabled: isDisabled, busy: loading ?? false }} |
There was a problem hiding this comment.
P2: accessibilityState can be overridden by caller-supplied props due to spread ordering
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/IconButton/IconButton.tsx, line 110:
<comment>accessibilityState can be overridden by caller-supplied props due to spread ordering</comment>
<file context>
@@ -0,0 +1,142 @@
+ return (
+ <Button
+ accessibilityLabel={accessibilityLabel}
+ accessibilityState={{ disabled: isDisabled, busy: loading ?? false }}
+ disabled={isDisabled}
+ variant={vendorVariantByOurVariant[resolvedVariant]}
</file context>
| )} | ||
| {...props} | ||
| > | ||
| <RadioGroupItem value={value} aria-label={label} /> |
There was a problem hiding this comment.
P2: Disabled state is not propagated to the nested RadioGroupItem, so tapping the radio control itself can still select the option even when the outer row is disabled.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx, line 87:
<comment>Disabled state is not propagated to the nested `RadioGroupItem`, so tapping the radio control itself can still select the option even when the outer row is disabled.</comment>
<file context>
@@ -0,0 +1,102 @@
+ )}
+ {...props}
+ >
+ <RadioGroupItem value={value} aria-label={label} />
+ <View className="flex-1 flex-row items-center gap-2">
+ <Text className="flex-1 font-medium text-foreground" numberOfLines={1}>
</file context>
| <RadioGroupItem value={value} aria-label={label} /> | |
| <RadioGroupItem value={value} aria-label={label} disabled={disabled} /> |
- Revert global.css and theme.ts to reactnativereusables default theme (stock shadcn/ui keys + values, no custom ember palette) - Remove DesignSystem gallery stories (Colors, Icons, Spacing, Typography) - Remove HelloWorld scaffold component - Remove design/manifest.json (superseded by Storybook) - Trim comments in global.css and main.js - Pin expo-font and expo-splash-screen to exact versions - Move tty stub from metro.config.js to Storybook module mock - Handle useFonts error so splash screen never stalls
Matches global.css and theme.ts exactly to origin/main — no custom theme changes in this PR, only Storybook tooling additions.
17beb7a to
4cdc402
Compare
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (2)
apps/mobile/components/CodeBlock/CodeBlock.tsx (1)
42-46:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winClean up setTimeout to prevent setState on unmounted component.
The
setTimeoutat line 45 is not cleaned up. If the component unmounts before 1500ms elapses, React will attempt to callsetCopied(false)on an unmounted component, causing warnings and a potential memory leak.🔧 Proposed fix using useEffect cleanup
+import { useEffect, useRef } from "react"; import { Check, Copy } from "lucide-react-native"; -import { useState } from "react"; +import { useState } from "react"; import { ScrollView, View, type ViewProps } from "react-native"; export function CodeBlock({ code, language, onCopy, overflow, bare, className, ...props }: CodeBlockProps) { const [copied, setCopied] = useState(false); + const timeoutRef = useRef<NodeJS.Timeout>(); const handleCopy = () => { onCopy?.(code); setCopied(true); - setTimeout(() => setCopied(false), 1500); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), 1500); }; + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/CodeBlock/CodeBlock.tsx` around lines 42 - 46, The setTimeout in handleCopy uses setCopied(false) after 1500ms but isn't cleared on unmount, so store the timer id (e.g., in a timerRef) when calling setTimeout inside handleCopy and clear it in a useEffect cleanup (or clearTimeout in a cleanup function returned from useEffect) to avoid calling setState on an unmounted component; update CodeBlock's handleCopy, add a ref like timerRef and a useEffect that clears timerRef.current on unmount and after clearing to prevent leaks.apps/mobile/components/PendingActionPill/PendingActionPill.tsx (1)
103-105:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUnmount condition is tied to a non-reactive value.
At Line 103, unmounting depends on
opacity.value, which won’t trigger a React re-render when the timing animation finishes. This can leave the component mounted at 0 opacity. Use React state for mount/unmount and update it from the animation completion callback.Proposed fix
-import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { Pressable, type PressableProps } from "react-native"; import Animated, { + runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; @@ }: PendingActionPillProps) { + const [isMounted, setIsMounted] = useState(visible); const cfg = KIND[kind]; @@ const opacity = useSharedValue(visible ? 1 : 0); const translateY = useSharedValue(visible ? 0 : 8); useEffect(() => { - opacity.value = withTiming(visible ? 1 : 0, { duration: 200 }); + if (visible) { + setIsMounted(true); + } + opacity.value = withTiming(visible ? 1 : 0, { duration: 200 }, (finished) => { + if (finished && !visible) { + runOnJS(setIsMounted)(false); + } + }); translateY.value = withTiming(visible ? 0 : 8, { duration: 200 }); }, [opacity, translateY, visible]); @@ - if (!visible && opacity.value === 0) { + if (!isMounted) { return null; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx` around lines 103 - 105, The component currently unmounts based on a non-reactive Reanimated value (opacity.value) inside the PendingActionPill render, so the React tree won’t update when the animation completes; replace that logic with React state (e.g. useState mounted/isMounted) and update that state from the animation completion callback (use runOnJS or the withTiming onEnd callback tied to the opacity animation) so when visible becomes false you animate opacity and then call the JS callback to set mounted=false to actually unmount; update the conditional that returns null to check visible and the new state instead of opacity.value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile/.rnstorybook/main.js`:
- Around line 3-6: The Storybook `stories` array in
apps/mobile/.rnstorybook/main.js omits the app's screen stories; add a glob for
the screens (e.g. "../screens/**/*.stories.?(ts|tsx|js|jsx)" or
"./screens/**/*.stories.?(ts|tsx|js|jsx)" depending on relative layout) to the
existing stories array so apps/mobile/screens/**/*.stories.* are discovered;
update the `stories` array entry near the existing
"./stories/**/*.stories.?(ts|tsx|js|jsx)" and
"../components/**/*.stories.?(ts|tsx|js|jsx)" entries.
In `@apps/mobile/components/AppHeader/AppHeader.tsx`:
- Around line 37-43: The header currently renders tappable back/action controls
even when callbacks are undefined; update AppHeader so the back and actions
buttons are only rendered as interactive when their respective handlers (onBack,
onActions) are provided — otherwise render a non-interactive element or a
disabled button (no onPress) and ensure accessibilityLabel remains present but
not actionable; locate the back button and actions button render logic in
AppHeader (props: showBack, onBack, backAccessibilityLabel and showActions,
onActions, actionsAccessibilityLabel, actionsIcon) and conditionally attach
onPress/interactive wrappers or set disabled/pointerEvents accordingly to
prevent no-op taps.
- Line 67: In AppHeader replace the fragile "w-1" spacer Views with a
fixed-width spacer that matches the header icon button slot (use the same width
used by the IconButton component or the shared ICON_BUTTON_SIZE constant), so
the title stays centered when a side control is hidden; update both spacer
occurrences around the title (the two View elements currently using
className="w-1") to use that exact fixed width via className or inline style
referencing the shared button size.
In `@apps/mobile/components/IconButton/IconButton.tsx`:
- Around line 125-139: The ActivityIndicator inside IconButton should use the
native color prop instead of passing a NativeWind className; update the loading
branch in the IconButton component to map resolvedVariant via the existing
iconColorByVariant (or a new color mapping) and pass that string into
ActivityIndicator's color prop rather than className, keeping the Icon branch
unchanged (Icon, iconSizeBySize, iconClassName remain as-is).
In `@apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx`:
- Around line 55-57: The detailed prop is documented to enable internal
scrolling but the detailed branch only caps height without overflow; update the
JSX that renders the args/preview for the detailed mode (the component/element
using the detailed?: boolean prop — e.g., the Args/args preview node inside
PendingApprovalCard) to wrap the preview text in a scroll container when
detailed is true and apply a maxHeight (~120px) plus overflowY: 'auto' (or
overflow: 'auto') so long previews scroll internally; implement this by adding a
wrapper div / styled component around the same preview markup that currently
sets height constraints and move the max-height + overflow styles there for the
detailed branch only.
In `@apps/mobile/components/Pill/Pill.tsx`:
- Around line 141-174: The dismiss Pressable currently calls onDismiss directly
which allows the tap to bubble to the outer Pressable (onPress) when
resolvedInteractive is true; change the inner Pressable's onPress to accept the
press event and call event.stopPropagation() (or event.stopPropagation?.())
before invoking onDismiss (e.g., onPress={(e) => { e.stopPropagation?.();
onDismiss?.(); }}), so the dismiss action does not trigger the parent Pill
onPress; update the inner Pressable in Pill.tsx where onDismiss is used and
ensure the event parameter type is compatible if needed.
In `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx`:
- Around line 32-46: The computed className for SuggestedAnswerPill (variable
className) can be overridden by spreading {...props}; update SuggestedAnswerPill
to merge className with any incoming props.className before passing to Pill,
e.g. derive a finalClassName by combining the local className (when variant ===
"ghost") and props.className (using a class merge helper or simple join) and
pass finalClassName to Pill instead of passing className and {...props} that may
overwrite it; keep the rest of the props spread as-is and ensure you reference
the className variable and the Pill component props in the change.
In `@apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx`:
- Around line 35-46: The running and pending style objects in ToolStatusRule use
iOS-only shadow props (shadowOpacity/shadowRadius/shadowOffset) which don't
produce the same glow on Android; update the running and pending styles to
include Android-compatible fallbacks (e.g., add elevation values and/or
Platform.select to set elevation for Android) and optionally provide a boxShadow
alternative when New Architecture is enabled; target the running and pending
style objects in ToolStatusRule to make the change so Android shows a comparable
glow.
In `@apps/mobile/components/ui/textarea.stories.tsx`:
- Around line 2-18: TextareaShowcase initializes local state with
useState(initialValue) so changes to the Storybook arg initialValue aren't
reflected; update the component to watch for changes to the initialValue prop
(the initialValue arg/control) and call setValue when it changes (e.g., add a
useEffect that has initialValue in its dependency array and updates value via
setValue), keeping existing state updates from user input intact; locate the
TextareaShowcase function and its useState(initialValue) to add this effect.
In `@apps/mobile/components/ui/toggle.stories.tsx`:
- Around line 3-20: The ToggleShowcase component currently initializes local
state with useState(initialPressed) but doesn't resync when story args change;
update ToggleShowcase to watch initialPressed and call
setPressed(initialPressed) via a useEffect so the local pressed state follows
changes to the initialPressed prop, leaving the Toggle props (pressed and
onPressedChange) unchanged; reference ToggleShowcase, initialPressed, pressed,
setPressed, useState, useEffect, and the Toggle component to locate where to add
the effect.
In `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx`:
- Around line 70-80: The retry Button is rendered even when there is no onRetry
handler, producing a non-functional CTA; update the UserMessageBubble render
logic to only show the retry control when both failed is true and onRetry is
provided (e.g., guard rendering of the Button/Text by checking failed &&
onRetry), and ensure the onRetry prop on the component is optional in its type
so callers aren't required to pass it; locate the conditional using failed and
the Button in UserMessageBubble to apply this guard.
---
Duplicate comments:
In `@apps/mobile/components/CodeBlock/CodeBlock.tsx`:
- Around line 42-46: The setTimeout in handleCopy uses setCopied(false) after
1500ms but isn't cleared on unmount, so store the timer id (e.g., in a timerRef)
when calling setTimeout inside handleCopy and clear it in a useEffect cleanup
(or clearTimeout in a cleanup function returned from useEffect) to avoid calling
setState on an unmounted component; update CodeBlock's handleCopy, add a ref
like timerRef and a useEffect that clears timerRef.current on unmount and after
clearing to prevent leaks.
In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx`:
- Around line 103-105: The component currently unmounts based on a non-reactive
Reanimated value (opacity.value) inside the PendingActionPill render, so the
React tree won’t update when the animation completes; replace that logic with
React state (e.g. useState mounted/isMounted) and update that state from the
animation completion callback (use runOnJS or the withTiming onEnd callback tied
to the opacity animation) so when visible becomes false you animate opacity and
then call the JS callback to set mounted=false to actually unmount; update the
conditional that returns null to check visible and the new state instead of
opacity.value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d87ee91f-2d1b-4ef2-8a8e-8f1ce002b1b2
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (141)
apps/mobile/.rnstorybook/.gitignoreapps/mobile/.rnstorybook/StorybookRouterProvider.tsxapps/mobile/.rnstorybook/index.tsxapps/mobile/.rnstorybook/main.jsapps/mobile/.rnstorybook/mocks/tty.jsapps/mobile/.rnstorybook/preview.tsxapps/mobile/.rnstorybook/router/LinkingContext.tsapps/mobile/.rnstorybook/router/UnhandledLinkingContext.tsapps/mobile/app/_layout.tsxapps/mobile/components/AppHeader/AppHeader.stories.tsxapps/mobile/components/AppHeader/AppHeader.tsxapps/mobile/components/AppHeader/index.tsapps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsxapps/mobile/components/ApprovalFooter/ApprovalFooter.tsxapps/mobile/components/ApprovalFooter/index.tsapps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsxapps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsxapps/mobile/components/AssistantMessageHead/index.tsapps/mobile/components/Banner/Banner.stories.tsxapps/mobile/components/Banner/Banner.tsxapps/mobile/components/Banner/index.tsapps/mobile/components/CodeBlock/CodeBlock.stories.tsxapps/mobile/components/CodeBlock/CodeBlock.tsxapps/mobile/components/CodeBlock/index.tsapps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsxapps/mobile/components/CollapsedBlock/CollapsedBlock.tsxapps/mobile/components/CollapsedBlock/index.tsapps/mobile/components/ComposerRow/ComposerRow.stories.tsxapps/mobile/components/ComposerRow/ComposerRow.tsxapps/mobile/components/ComposerRow/index.tsapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsxapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsxapps/mobile/components/ComposerSettingsButton/index.tsapps/mobile/components/FabBase/FabBase.stories.tsxapps/mobile/components/FabBase/FabBase.tsxapps/mobile/components/FabBase/index.tsapps/mobile/components/HitTargetWrapper/HitTargetWrapper.stories.tsxapps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsxapps/mobile/components/HitTargetWrapper/index.tsapps/mobile/components/IconButton/IconButton.stories.tsxapps/mobile/components/IconButton/IconButton.tsxapps/mobile/components/IconButton/index.tsapps/mobile/components/ModalHeader/ModalHeader.stories.tsxapps/mobile/components/ModalHeader/ModalHeader.tsxapps/mobile/components/ModalHeader/index.tsapps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsxapps/mobile/components/ModelPickerOption/ModelPickerOption.tsxapps/mobile/components/ModelPickerOption/index.tsapps/mobile/components/PendingActionPill/PendingActionPill.stories.tsxapps/mobile/components/PendingActionPill/PendingActionPill.tsxapps/mobile/components/PendingActionPill/index.tsapps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsxapps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsxapps/mobile/components/PendingApprovalCard/index.tsapps/mobile/components/PickerTrigger/PickerTrigger.stories.tsxapps/mobile/components/PickerTrigger/PickerTrigger.tsxapps/mobile/components/PickerTrigger/index.tsapps/mobile/components/Pill/Pill.stories.tsxapps/mobile/components/Pill/Pill.tsxapps/mobile/components/Pill/index.tsapps/mobile/components/ProgressDots/ProgressDots.stories.tsxapps/mobile/components/ProgressDots/ProgressDots.tsxapps/mobile/components/ProgressDots/index.tsapps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsxapps/mobile/components/ScrollBackButton/ScrollBackButton.tsxapps/mobile/components/ScrollBackButton/index.tsapps/mobile/components/ScrollFade/ScrollFade.stories.tsxapps/mobile/components/ScrollFade/ScrollFade.tsxapps/mobile/components/ScrollFade/index.tsapps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsxapps/mobile/components/SlashCommandOption/SlashCommandOption.tsxapps/mobile/components/SlashCommandOption/index.tsapps/mobile/components/StatusDot/StatusDot.stories.tsxapps/mobile/components/StatusDot/StatusDot.tsxapps/mobile/components/StatusDot/index.tsapps/mobile/components/StreamingCursor/StreamingCursor.stories.tsxapps/mobile/components/StreamingCursor/StreamingCursor.tsxapps/mobile/components/StreamingCursor/index.tsapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsxapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsxapps/mobile/components/SuggestedAnswerPill/index.tsapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsxapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsxapps/mobile/components/ThinkingLevelOption/index.tsapps/mobile/components/ToastBase/ToastBase.stories.tsxapps/mobile/components/ToastBase/ToastBase.tsxapps/mobile/components/ToastBase/index.tsapps/mobile/components/ToolCallCard/ToolCallCard.stories.tsxapps/mobile/components/ToolCallCard/ToolCallCard.tsxapps/mobile/components/ToolCallCard/index.tsapps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsxapps/mobile/components/ToolStatusRule/ToolStatusRule.tsxapps/mobile/components/ToolStatusRule/index.tsapps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsxapps/mobile/components/UserMessageBubble/UserMessageBubble.tsxapps/mobile/components/UserMessageBubble/index.tsapps/mobile/components/ui/AUDIT.mdapps/mobile/components/ui/accordion.stories.tsxapps/mobile/components/ui/alert-dialog.stories.tsxapps/mobile/components/ui/alert.stories.tsxapps/mobile/components/ui/aspect-ratio.stories.tsxapps/mobile/components/ui/avatar.stories.tsxapps/mobile/components/ui/badge.stories.tsxapps/mobile/components/ui/button.stories.tsxapps/mobile/components/ui/card.stories.tsxapps/mobile/components/ui/checkbox.stories.tsxapps/mobile/components/ui/collapsible.stories.tsxapps/mobile/components/ui/context-menu.stories.tsxapps/mobile/components/ui/dialog.stories.tsxapps/mobile/components/ui/dropdown-menu.stories.tsxapps/mobile/components/ui/hover-card.stories.tsxapps/mobile/components/ui/icon.stories.tsxapps/mobile/components/ui/input.stories.tsxapps/mobile/components/ui/label.stories.tsxapps/mobile/components/ui/menubar.stories.tsxapps/mobile/components/ui/popover.stories.tsxapps/mobile/components/ui/progress.stories.tsxapps/mobile/components/ui/radio-group.stories.tsxapps/mobile/components/ui/select.stories.tsxapps/mobile/components/ui/separator.stories.tsxapps/mobile/components/ui/skeleton.stories.tsxapps/mobile/components/ui/switch.stories.tsxapps/mobile/components/ui/tabs.stories.tsxapps/mobile/components/ui/text.stories.tsxapps/mobile/components/ui/textarea.stories.tsxapps/mobile/components/ui/toggle-group.stories.tsxapps/mobile/components/ui/toggle.stories.tsxapps/mobile/components/ui/tooltip.stories.tsxapps/mobile/metro.config.jsapps/mobile/package.jsonapps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsxapps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsxapps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.stories.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsxapps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsxapps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsxapps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsxapps/mobile/screens/AUDIT.md
✅ Files skipped from review due to trivial changes (28)
- apps/mobile/.rnstorybook/.gitignore
- apps/mobile/components/CodeBlock/index.ts
- apps/mobile/.rnstorybook/mocks/tty.js
- apps/mobile/components/ModalHeader/index.ts
- apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts
- apps/mobile/components/ToolStatusRule/index.ts
- apps/mobile/components/AppHeader/AppHeader.stories.tsx
- apps/mobile/.rnstorybook/router/LinkingContext.ts
- apps/mobile/components/UserMessageBubble/index.ts
- apps/mobile/components/ScrollBackButton/index.ts
- apps/mobile/components/Banner/index.ts
- apps/mobile/components/AppHeader/index.ts
- apps/mobile/components/ThinkingLevelOption/index.ts
- apps/mobile/components/AssistantMessageHead/index.ts
- apps/mobile/components/CollapsedBlock/index.ts
- apps/mobile/components/SlashCommandOption/index.ts
- apps/mobile/components/ComposerSettingsButton/index.ts
- apps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsx
- apps/mobile/screens/AUDIT.md
- apps/mobile/components/ModelPickerOption/index.ts
- apps/mobile/components/ui/AUDIT.md
- apps/mobile/components/ToastBase/index.ts
- apps/mobile/components/StatusDot/StatusDot.stories.tsx
- apps/mobile/components/ToolCallCard/index.ts
- apps/mobile/components/ui/dialog.stories.tsx
- apps/mobile/components/FabBase/index.ts
- apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
- apps/mobile/components/ApprovalFooter/index.ts
| stories: [ | ||
| "./stories/**/*.stories.?(ts|tsx|js|jsx)", | ||
| "../components/**/*.stories.?(ts|tsx|js|jsx)", | ||
| ], |
There was a problem hiding this comment.
Include screens stories in Storybook discovery.
The current stories globs skip apps/mobile/screens/**/*.stories.*, so those stories in this cohort won’t load.
Suggested diff
const main = {
stories: [
"./stories/**/*.stories.?(ts|tsx|js|jsx)",
"../components/**/*.stories.?(ts|tsx|js|jsx)",
+ "../screens/**/*.stories.?(ts|tsx|js|jsx)",
],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| stories: [ | |
| "./stories/**/*.stories.?(ts|tsx|js|jsx)", | |
| "../components/**/*.stories.?(ts|tsx|js|jsx)", | |
| ], | |
| stories: [ | |
| "./stories/**/*.stories.?(ts|tsx|js|jsx)", | |
| "../components/**/*.stories.?(ts|tsx|js|jsx)", | |
| "../screens/**/*.stories.?(ts|tsx|js|jsx)", | |
| ], |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/.rnstorybook/main.js` around lines 3 - 6, The Storybook `stories`
array in apps/mobile/.rnstorybook/main.js omits the app's screen stories; add a
glob for the screens (e.g. "../screens/**/*.stories.?(ts|tsx|js|jsx)" or
"./screens/**/*.stories.?(ts|tsx|js|jsx)" depending on relative layout) to the
existing stories array so apps/mobile/screens/**/*.stories.* are discovered;
update the `stories` array entry near the existing
"./stories/**/*.stories.?(ts|tsx|js|jsx)" and
"../components/**/*.stories.?(ts|tsx|js|jsx)" entries.
| showBack = true, | ||
| onBack, | ||
| backAccessibilityLabel = "Back to sessions", | ||
| showActions = true, | ||
| onActions, | ||
| actionsAccessibilityLabel = "Session actions", | ||
| actionsIcon = MoreVertical, |
There was a problem hiding this comment.
Prevent no-op header actions when callbacks are missing.
Both action buttons can render as tappable controls even when onBack/onActions are undefined, which creates dead interactions.
Suggested diff
- showBack = true,
+ showBack = true,
onBack,
@@
- showActions = true,
+ showActions = true,
onActions,
@@
- {showBack ? (
+ {showBack && onBack ? (
<IconButton
@@
- {showActions ? (
+ {showActions && onActions ? (
<IconButtonAlso applies to: 58-65, 86-93
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/AppHeader/AppHeader.tsx` around lines 37 - 43, The
header currently renders tappable back/action controls even when callbacks are
undefined; update AppHeader so the back and actions buttons are only rendered as
interactive when their respective handlers (onBack, onActions) are provided —
otherwise render a non-interactive element or a disabled button (no onPress) and
ensure accessibilityLabel remains present but not actionable; locate the back
button and actions button render logic in AppHeader (props: showBack, onBack,
backAccessibilityLabel and showActions, onActions, actionsAccessibilityLabel,
actionsIcon) and conditionally attach onPress/interactive wrappers or set
disabled/pointerEvents accordingly to prevent no-op taps.
| onPress={onBack} | ||
| /> | ||
| ) : ( | ||
| <View className="w-1" /> |
There was a problem hiding this comment.
Use fixed-width spacers matching icon-button width.
w-1 placeholders can shift title alignment when one side control is hidden. Use a stable spacer width equal to the button slot.
Suggested diff
- <View className="w-1" />
+ <View className="w-touch-min" />
@@
- <View className="w-1" />
+ <View className="w-touch-min" />Also applies to: 95-95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/AppHeader/AppHeader.tsx` at line 67, In AppHeader
replace the fragile "w-1" spacer Views with a fixed-width spacer that matches
the header icon button slot (use the same width used by the IconButton component
or the shared ICON_BUTTON_SIZE constant), so the title stays centered when a
side control is hidden; update both spacer occurrences around the title (the two
View elements currently using className="w-1") to use that exact fixed width via
className or inline style referencing the shared button size.
| {loading ? ( | ||
| <ActivityIndicator | ||
| size="small" | ||
| className={iconColorByVariant[resolvedVariant]} | ||
| /> | ||
| ) : ( | ||
| <Icon | ||
| as={icon} | ||
| className={cn( | ||
| iconSizeBySize[resolvedSize], | ||
| iconColorByVariant[resolvedVariant], | ||
| iconClassName, | ||
| )} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
Use native color prop instead of className for ActivityIndicator.
React Native's ActivityIndicator expects a color prop (hex or named color string), not className-based styling. The NativeWind className approach on line 128 may not reliably set the spinner color across variants.
🎨 Proposed fix to use native color prop
Define a color mapping:
const iconColorByVariant: Record<IconButtonVariant, string> = {
ghost: "text-foreground",
soft: "text-foreground",
primary: "text-primary-foreground",
neutral: "text-background",
destructive: "text-white",
};
+
+const spinnerColorByVariant: Record<IconButtonVariant, string> = {
+ ghost: "`#000000`", // or theme color variable
+ soft: "`#000000`",
+ primary: "`#ffffff`",
+ neutral: "`#ffffff`",
+ destructive: "`#ffffff`",
+};Then use it:
{loading ? (
<ActivityIndicator
size="small"
- className={iconColorByVariant[resolvedVariant]}
+ color={spinnerColorByVariant[resolvedVariant]}
/>
) : (Alternatively, if your theme system provides color tokens, extract them programmatically rather than using className.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/IconButton/IconButton.tsx` around lines 125 - 139, The
ActivityIndicator inside IconButton should use the native color prop instead of
passing a NativeWind className; update the loading branch in the IconButton
component to map resolvedVariant via the existing iconColorByVariant (or a new
color mapping) and pass that string into ActivityIndicator's color prop rather
than className, keeping the Icon branch unchanged (Icon, iconSizeBySize,
iconClassName remain as-is).
| /** Internal scroll on the args preview when multi-line (≤120pt tall). */ | ||
| detailed?: boolean; | ||
| }; |
There was a problem hiding this comment.
detailed mode doesn’t implement the documented internal scroll.
Line 55 promises internal scrolling, but Lines 119-125 only cap text height. Add a scroll container for the detailed branch so long previews remain readable.
Proposed fix
-import { View, type ViewProps } from "react-native";
+import { ScrollView, View, type ViewProps } from "react-native";
@@
{argsPreview ? (
<>
<Separator />
- <Text
- className={cn(
- "font-mono text-xs text-foreground",
- detailed && "max-h-32",
- )}
- numberOfLines={detailed ? undefined : 3}
- >
- {argsPreview}
- </Text>
+ {detailed ? (
+ <ScrollView className="max-h-32" nestedScrollEnabled>
+ <Text className="font-mono text-xs text-foreground">
+ {argsPreview}
+ </Text>
+ </ScrollView>
+ ) : (
+ <Text className="font-mono text-xs text-foreground" numberOfLines={3}>
+ {argsPreview}
+ </Text>
+ )}
</>
) : null}Also applies to: 119-125
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx` around
lines 55 - 57, The detailed prop is documented to enable internal scrolling but
the detailed branch only caps height without overflow; update the JSX that
renders the args/preview for the detailed mode (the component/element using the
detailed?: boolean prop — e.g., the Args/args preview node inside
PendingApprovalCard) to wrap the preview text in a scroll container when
detailed is true and apply a maxHeight (~120px) plus overflowY: 'auto' (or
overflow: 'auto') so long previews scroll internally; implement this by adding a
wrapper div / styled component around the same preview markup that currently
sets height constraints and move the max-height + overflow styles there for the
detailed branch only.
| const className = | ||
| variant === "ghost" ? "bg-transparent border-border" : undefined; | ||
|
|
||
| return ( | ||
| <Pill | ||
| label={text} | ||
| variant={pillVariant} | ||
| size="md" | ||
| interactive | ||
| onPress={onPress} | ||
| disabled={disabled} | ||
| accessibilityLabel={`Use suggested answer: ${text}`} | ||
| className={className} | ||
| {...props} | ||
| /> |
There was a problem hiding this comment.
Preserve ghost styles when consumer passes className
At Line 45, {...props} can override Line 44’s computed className, so variant="ghost" styles may be dropped when callers pass their own class. Merge classes explicitly instead.
Suggested fix
import { Pill, type PillProps } from "`@/components/Pill`";
+import { cn } from "`@/lib/utils`";
@@
export function SuggestedAnswerPill({
text,
variant = "default",
onPress,
disabled,
+ className: userClassName,
...props
}: SuggestedAnswerPillProps) {
const pillVariant: PillProps["variant"] =
variant === "accent" ? "accent" : "default";
- const className =
+ const variantClassName =
variant === "ghost" ? "bg-transparent border-border" : undefined;
@@
onPress={onPress}
disabled={disabled}
accessibilityLabel={`Use suggested answer: ${text}`}
- className={className}
+ className={cn(variantClassName, userClassName)}
{...props}
/>
);
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx` around
lines 32 - 46, The computed className for SuggestedAnswerPill (variable
className) can be overridden by spreading {...props}; update SuggestedAnswerPill
to merge className with any incoming props.className before passing to Pill,
e.g. derive a finalClassName by combining the local className (when variant ===
"ghost") and props.className (using a class merge helper or simple join) and
pass finalClassName to Pill instead of passing className and {...props} that may
overwrite it; keep the rest of the props spread as-is and ensure you reference
the className variable and the Pill component props in the change.
| running: { | ||
| shadowColor: "rgba(80, 168, 120, 0.6)", | ||
| shadowOpacity: 1, | ||
| shadowRadius: 6, | ||
| shadowOffset: { width: 0, height: 0 }, | ||
| }, | ||
| pending: { | ||
| shadowColor: "rgba(212, 168, 75, 0.6)", | ||
| shadowOpacity: 1, | ||
| shadowRadius: 4, | ||
| shadowOffset: { width: 0, height: 0 }, | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In React Native 0.85.x, which shadow style props are supported on Android (shadowOpacity, shadowRadius, shadowOffset), and is boxShadow the recommended cross-platform alternative?
💡 Result:
In React Native 0.85, the legacy shadow style props (shadowOpacity, shadowRadius, shadowOffset) remain effectively iOS-only [1][2][3]. While shadowColor is supported on both platforms [1][3], the other three props do not function on Android [1][3]. boxShadow is the recommended, modern, and cross-platform alternative for handling shadows in React Native [1][4]. Key details regarding boxShadow in React Native 0.85: - It is a spec-compliant implementation of the web style prop [1]. - It is supported on both iOS and Android [1]. - It is available exclusively under the New Architecture [5]. - Unlike legacy shadow props, boxShadow supports more complex features such as inset shadows and multiple shadow layers [5][1]. - For Android specifically, boxShadow has version-dependent support: outset shadows are supported on Android 9+, and inset shadows are supported on Android 10+ [5]. If you are not using the New Architecture or require support for older Android versions where boxShadow is unavailable, the traditional approach remains using the elevation prop for Android, often combined with conditional platform styling to apply shadow* props on iOS [6][7][2].
Citations:
- 1: https://reactnative.dev/docs/shadow-props
- 2: https://simpletool.io/blog/react-native-shadow-generator/
- 3: https://www.tricentis.com/learn/react-native-shadow
- 4: boxShadow support on New Architecture nativewind/nativewind#1512
- 5: https://reactnative.dev/docs/0.84/view-style-props
- 6: https://stackoverflow.com/questions/52500541/react-native-shadow-props-for-android
- 7: https://stackoverflow.com/questions/73890654/react-native-ios-and-android-box-shadow
Fix Android glow styling for running/pending in ToolStatusRule
In React Native, the legacy shadow props you’re using (shadowOpacity, shadowRadius, shadowOffset) are effectively iOS-only on Android; only shadowColor is supported there—so the glow will likely not match on Android. Add an Android fallback (e.g., elevation) and/or switch to boxShadow for cross-platform shadows (noting boxShadow support is tied to the New Architecture).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx` around lines 35 -
46, The running and pending style objects in ToolStatusRule use iOS-only shadow
props (shadowOpacity/shadowRadius/shadowOffset) which don't produce the same
glow on Android; update the running and pending styles to include
Android-compatible fallbacks (e.g., add elevation values and/or Platform.select
to set elevation for Android) and optionally provide a boxShadow alternative
when New Architecture is enabled; target the running and pending style objects
in ToolStatusRule to make the change so Android shows a comparable glow.
| import { useState } from "react"; | ||
| import { View } from "react-native"; | ||
| import { Textarea } from "@/components/ui/textarea"; | ||
|
|
||
| function TextareaShowcase({ | ||
| placeholder, | ||
| editable, | ||
| initialValue, | ||
| numberOfLines, | ||
| }: { | ||
| placeholder: string; | ||
| editable: boolean; | ||
| initialValue: string; | ||
| numberOfLines: number; | ||
| }) { | ||
| const [value, setValue] = useState(initialValue); | ||
| return ( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and display the relevant Storybook story file with line numbers
FILE="apps/mobile/components/ui/textarea.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "==== $FILE (head/tail excerpt) ===="
wc -l "$FILE"
sed -n '1,200p' "$FILE" | nl -ba | sed -n '1,220p'Repository: superset-sh/superset
Length of output: 253
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
wc -l "$FILE"
sed -n '1,220p' "$FILE" | nl -ba | sed -n '1,220p'Repository: superset-sh/superset
Length of output: 175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
sed -n '1,120p' "$FILE" | nl -ba
sed -n '120,260p' "$FILE" | nl -baRepository: superset-sh/superset
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
wc -l "$FILE"
echo "----- BEGIN -----"
sed -n '1,220p' "$FILE" | nl -ba
echo "----- END -----"Repository: superset-sh/superset
Length of output: 193
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
wc -l "$FILE"
sed -n '1,220p' "$FILE" | nl -baRepository: superset-sh/superset
Length of output: 175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
wc -l "$FILE"
sed -n '1,260p' "$FILE" | nl -baRepository: superset-sh/superset
Length of output: 175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
sed -n '1,260p' "$FILE" | nl -baRepository: superset-sh/superset
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
sed -n '1,120p' "$FILE" | nl -baRepository: superset-sh/superset
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
echo "=== File: $FILE ==="
wc -l "$FILE"
echo "=== Lines 1-120 (awk line numbers) ==="
awk '{printf "%5d\t%s\n", NR, $0}' "$FILE" | sed -n '1,120p'
echo "=== Context around TextareaShowcase ==="
rg -n "function TextareaShowcase|TextareaShowcase|initialValue|useState\\(initialValue\\)" "$FILE" || true
echo "=== Context around default export / meta / args ==="
rg -n "export default|args|argTypes|render" "$FILE" || trueRepository: superset-sh/superset
Length of output: 3572
Sync Storybook initialValue into local state in TextareaShowcase.
TextareaShowcase uses useState(initialValue) only once, so updates to the Storybook initialValue args/control won’t update the textarea value.
Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const [value, setValue] = useState(initialValue);
+ useEffect(() => {
+ setValue(initialValue);
+ }, [initialValue]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { useState } from "react"; | |
| import { View } from "react-native"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| function TextareaShowcase({ | |
| placeholder, | |
| editable, | |
| initialValue, | |
| numberOfLines, | |
| }: { | |
| placeholder: string; | |
| editable: boolean; | |
| initialValue: string; | |
| numberOfLines: number; | |
| }) { | |
| const [value, setValue] = useState(initialValue); | |
| return ( | |
| import { useEffect, useState } from "react"; | |
| import { View } from "react-native"; | |
| import { Textarea } from "`@/components/ui/textarea`"; | |
| function TextareaShowcase({ | |
| placeholder, | |
| editable, | |
| initialValue, | |
| numberOfLines, | |
| }: { | |
| placeholder: string; | |
| editable: boolean; | |
| initialValue: string; | |
| numberOfLines: number; | |
| }) { | |
| const [value, setValue] = useState(initialValue); | |
| useEffect(() => { | |
| setValue(initialValue); | |
| }, [initialValue]); | |
| return ( |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ui/textarea.stories.tsx` around lines 2 - 18,
TextareaShowcase initializes local state with useState(initialValue) so changes
to the Storybook arg initialValue aren't reflected; update the component to
watch for changes to the initialValue prop (the initialValue arg/control) and
call setValue when it changes (e.g., add a useEffect that has initialValue in
its dependency array and updates value via setValue), keeping existing state
updates from user input intact; locate the TextareaShowcase function and its
useState(initialValue) to add this effect.
| import { useState } from "react"; | ||
| import { Text } from "@/components/ui/text"; | ||
| import { Toggle, ToggleIcon } from "@/components/ui/toggle"; | ||
|
|
||
| function ToggleShowcase({ | ||
| variant, | ||
| withIcon, | ||
| label, | ||
| initialPressed, | ||
| }: { | ||
| variant: "default" | "outline"; | ||
| withIcon: boolean; | ||
| label: string; | ||
| initialPressed: boolean; | ||
| }) { | ||
| const [pressed, setPressed] = useState(initialPressed); | ||
| return ( | ||
| <Toggle variant={variant} pressed={pressed} onPressedChange={setPressed}> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and print the full toggle story file with line numbers
FILE="apps/mobile/components/ui/toggle.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "=== $FILE (lines) ==="
nl -ba "$FILE" | sed -n '1,220p'
# Search for similar "initialPressed" usage in the repo (to cross-check pattern)
rg -n "initialPressed|pressedChange|onPressedChange|ToggleShowcase" -S apps/mobile/components/ui || trueRepository: superset-sh/superset
Length of output: 187
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/toggle.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "=== $FILE (first 220 lines, with line numbers via cat -n) ==="
cat -n "$FILE" | sed -n '1,220p'
echo "=== rg: initialPressed / args wiring (file + nearby components/ui stories) ==="
rg -n "initialPressed|args|ToggleShowcase|onPressedChange|pressed" -S "$FILE" || true
echo "=== rg: other stories for the same pattern (pressed state from args) ==="
rg -n "setPressed\\(|useState\\(initialPressed\\)|onPressedChange\\=\\{set.*\\}|useEffect\\(\\(\\) => \\{\\s*set.*Pressed|initialPressed\\:" -S apps/mobile/components/ui || trueRepository: superset-sh/superset
Length of output: 3205
Resync pressed when initialPressed story args change.
useState(initialPressed) only uses initialPressed on the initial mount; updating Storybook controls for initialPressed won’t update the rendered toggle state.
Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const [pressed, setPressed] = useState(initialPressed);
+ useEffect(() => {
+ setPressed(initialPressed);
+ }, [initialPressed]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { useState } from "react"; | |
| import { Text } from "@/components/ui/text"; | |
| import { Toggle, ToggleIcon } from "@/components/ui/toggle"; | |
| function ToggleShowcase({ | |
| variant, | |
| withIcon, | |
| label, | |
| initialPressed, | |
| }: { | |
| variant: "default" | "outline"; | |
| withIcon: boolean; | |
| label: string; | |
| initialPressed: boolean; | |
| }) { | |
| const [pressed, setPressed] = useState(initialPressed); | |
| return ( | |
| <Toggle variant={variant} pressed={pressed} onPressedChange={setPressed}> | |
| import { useEffect, useState } from "react"; | |
| import { Text } from "`@/components/ui/text`"; | |
| import { Toggle, ToggleIcon } from "`@/components/ui/toggle`"; | |
| function ToggleShowcase({ | |
| variant, | |
| withIcon, | |
| label, | |
| initialPressed, | |
| }: { | |
| variant: "default" | "outline"; | |
| withIcon: boolean; | |
| label: string; | |
| initialPressed: boolean; | |
| }) { | |
| const [pressed, setPressed] = useState(initialPressed); | |
| useEffect(() => { | |
| setPressed(initialPressed); | |
| }, [initialPressed]); | |
| return ( | |
| <Toggle variant={variant} pressed={pressed} onPressedChange={setPressed}> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ui/toggle.stories.tsx` around lines 3 - 20, The
ToggleShowcase component currently initializes local state with
useState(initialPressed) but doesn't resync when story args change; update
ToggleShowcase to watch initialPressed and call setPressed(initialPressed) via a
useEffect so the local pressed state follows changes to the initialPressed prop,
leaving the Toggle props (pressed and onPressedChange) unchanged; reference
ToggleShowcase, initialPressed, pressed, setPressed, useState, useEffect, and
the Toggle component to locate where to add the effect.
| {failed ? ( | ||
| <> | ||
| <Text className="text-xs text-state-danger-fg">Failed to send</Text> | ||
| <Button | ||
| size="sm" | ||
| variant="link" | ||
| className="h-auto px-1" | ||
| onPress={onRetry} | ||
| > | ||
| <Text className="text-xs text-primary">Retry</Text> | ||
| </Button> |
There was a problem hiding this comment.
Avoid rendering a non-functional Retry action when onRetry is missing.
On Lines 73-80, failed shows a retry control even if no callback is provided, creating an inert CTA in an error state.
💡 Proposed fix
{failed ? (
<>
<Text className="text-xs text-state-danger-fg">Failed to send</Text>
- <Button
- size="sm"
- variant="link"
- className="h-auto px-1"
- onPress={onRetry}
- >
- <Text className="text-xs text-primary">Retry</Text>
- </Button>
+ {onRetry ? (
+ <Button
+ size="sm"
+ variant="link"
+ className="h-auto px-1"
+ onPress={onRetry}
+ >
+ <Text className="text-xs text-primary">Retry</Text>
+ </Button>
+ ) : null}
</>
) : timestamp ? (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {failed ? ( | |
| <> | |
| <Text className="text-xs text-state-danger-fg">Failed to send</Text> | |
| <Button | |
| size="sm" | |
| variant="link" | |
| className="h-auto px-1" | |
| onPress={onRetry} | |
| > | |
| <Text className="text-xs text-primary">Retry</Text> | |
| </Button> | |
| {failed ? ( | |
| <> | |
| <Text className="text-xs text-state-danger-fg">Failed to send</Text> | |
| {onRetry ? ( | |
| <Button | |
| size="sm" | |
| variant="link" | |
| className="h-auto px-1" | |
| onPress={onRetry} | |
| > | |
| <Text className="text-xs text-primary">Retry</Text> | |
| </Button> | |
| ) : null} | |
| </> | |
| ) : timestamp ? ( |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx` around lines
70 - 80, The retry Button is rendered even when there is no onRetry handler,
producing a non-functional CTA; update the UserMessageBubble render logic to
only show the retry control when both failed is true and onRetry is provided
(e.g., guard rendering of the Button/Text by checking failed && onRetry), and
ensure the onRetry prop on the component is optional in its type so callers
aren't required to pass it; locate the conditional using failed and the Button
in UserMessageBubble to apply this guard.
- Remove duplicate NavigationContainer from preview.tsx decorator. StorybookRouterProvider already provides one. SDK 56 blocks direct @react-navigation/native imports inside the expo-router tree. - Add Welcome.stories.tsx so Storybook has at least one story on the tooling branch (prevents EmptyIndexError crash).
…migration)
Migration verification surface for the ember theme rewrite. Every primitive in
apps/mobile/components/ui/* now has a sidecar Components/{Primitive} story so
the live app components are visually inspectable on iOS Simulator + Android
Emulator under both light and dark ember themes.
Per the vendor-immutable rule, no primitive source file is edited — the stories
import the real components the app already uses. Discovered token-bypass
divergences (shadow-black/5, bg-black/50 backdrops, text-white in destructive
variants) are documented in apps/mobile/components/ui/AUDIT.md with the
upstream-PR resolution path. Local source edits to vendor primitives are
explicitly not the fix.
- 28 stories: accordion, alert, alert-dialog, aspect-ratio, avatar, badge,
button, card, checkbox, collapsible, context-menu, dialog, dropdown-menu,
hover-card, icon (catalog with 22 chat-domain lucide icons), input, label,
menubar, popover, progress, radio-group, select, separator, skeleton, switch,
tabs, text, textarea, toggle, toggle-group, tooltip
- AUDIT.md: 3 token-bypass findings catalogued, accepted as upstream-PR work
- .rnstorybook/preview.tsx: PortalHost added to decorator so primitives that
render via portal (Popover, Dialog, AlertDialog, DropdownMenu, ContextMenu,
Menubar, Select, Tooltip, HoverCard) work in Storybook
- Story conventions match the workflow spec: title='Components/{Name}',
argTypes wired for all controllable props, realistic chat-domain mock data
(model names, session statuses, branch · host disambiguation)
Verified: bun typecheck (exit 0), biome check (no warnings).
This is the migration verification layer for Sprint 01 Phase 0 (token rewrite,
2c21dbc). Pixel-perfect manifest atoms gate stays pending — the gate flips
only after the human reviewer walks the Storybook inventory on simulator.
…udit + stories) Completes the theme-migration story coverage for live first-party app components in apps/mobile/screens/**/components/. Per the audit pass: CRITICAL fixes (active bugs under ember): - OrganizationHeaderButton.tsx:25 — replaced hardcoded `hsl(240 5% 64.9%)` (OLD cool-neutral palette) with `theme.mutedForeground` (warm). Chevron now renders correctly under the warm-neutral surface ramp. MEDIUM fixes (token bypass / wrong source): - SocialButton.tsx — replaced `useColorScheme()` + hardcoded `"white"/"black"` icon color with `useTheme().foreground`. Previously the icon read the OS appearance setting (which may disagree with `Uniwind.setTheme()`); now tracks the active app theme. Google brand colors retained per brand-asset policy. DOCUMENTED exceptions: - SocialButton Google icon brand colors (#4285F4 / #34A853 / #FBBC05 / #EA4335) — kept as official Google brand assets. - OrganizationSwitcherSheet forces `colorScheme: dark` in SwiftUI environment. Documented as design choice; revisit if app supports system light/dark. Story coverage (8 stories) — sidecar to source per existing convention: - OrganizationAvatar — fully presentational; matrix story for sizes 20/28/36/48/64 - SocialButton — both providers; brand-color verification - DevSignInButton — base render only - OrganizationHeaderButton — *Preview wrapper renders inner content* (real component lives inside expo-router Stack.Toolbar — not isolatable) - OrganizationSwitcherSheet — *Inner content* + NativeSheet iOS-only story (real sheet uses @expo/ui/swift-ui — Android renders disclaimer) - OrgDropdown — *Preview wrapper* with mock orgs (real reads authClient + Electric collections — not isolatable) - TabBarAccessory — *Preview wrapper* with mock org (real reads useOrganizations) - AuthenticatedTabBar — placeholder documenting integration dependencies (TabBarView SwiftUI bridge + expo-router/ui Tabs context + Electric) Storybook glob — extended `.rnstorybook/main.js` to include `../screens/**/*.stories.?(ts|tsx|js|jsx)` so screen-component stories are picked up by sb-rn-get-stories. Audit doc — apps/mobile/screens/AUDIT.md catalogs all 8 components, the 2 fixes applied, the 2 documented exceptions, the 6 clean components, and the isolation caveats per integration-heavy component. This completes the migration of the live app component set to the ember theme. The Storybook walkthrough on iOS Simulator + Android Emulator is the verification surface. Verified: bun typecheck (exit 0), biome check (clean).
4cdc402 to
0f891ea
Compare
…s PR) The Welcome story was added in PR superset-sh#4874 (tooling) to give Storybook at least one entry — otherwise the empty stories index threw an EmptyIndexError on the tooling branch in isolation. This PR brings real component stories (8 first-party + 28 vendor primitives), so the placeholder is no longer needed.
Adds 5 chat-specific atom components + stories as foundation for the chat
mobile sprint. Token-driven (state palette, ember accent), 44pt touch targets,
no hardcoded values. These do NOT yet match the designs/atoms/{name}/ HTML
mockups exactly — a follow-up pass will refine each against the design spec
before chat screens consume them.
Why "foundation" not "migration": these are new components, not migration of
existing live app components to the new theme. They were started during the
build pass but the user redirected scope to migration-first; committing as
foundation rather than discarding so the work survives for future refinement.
- components/IconButton/ — Pressable + Icon wrapper with 44pt min hit target,
5 variants (default/primary/secondary/ghost/destructive), 3 sizes.
Real-design refinement TODO: match designs/atoms/icon-button/README.md
variant taxonomy (--ghost is default per spec, also needs --soft and
--neutral variants; pill shape modifier).
- components/Pill/ — chat-domain pill (model chip, mode chip, suggested answer)
with 6 variants (default/selected/warning/danger/success/live) and
interactive/non-interactive split.
Real-design refinement TODO: match designs/atoms/pill/README.md — needs
6 variants per spec (default/strong/accent/live/warning/danger), 3 sizes
with monospace + uppercase modifiers, separate dismiss button.
- components/StatusDot/ — single colored circle (live/warning/danger/success/
neutral + ember). 3 sizes.
Real-design refinement TODO: match designs/atoms/status-dot/README.md —
6px/8px/10px exact sizes (sm 8px is default), live variant needs pulse
animation, warning needs box-shadow ring.
- components/StreamingCursor/ — Reanimated ▌ blink. Configurable duration +
color. accessibilityElementsHidden per spec.
Real-design refinement TODO: match designs/atoms/streaming-cursor/README.md
— 2px width × 1em height with 6px glow shadow, 1s steps(2) animation,
--default / --steady / --paused variants.
- components/ToolStatusRule/ — 3px (default) left rule with running/completed/
failed/pending/neutral/ember variants.
Real-design refinement TODO: match designs/atoms/tool-status-rule/README.md
— 5 status variants (--running/--done/--pending/--error/--neutral), needs
--vertical/--horizontal orientation, glow shadow on running + pending.
All atoms have Components/{Name} stories with argTypes wired. Verified:
bun typecheck (exit 0), biome check (no warnings).
ScrollFade and HitTargetWrapper from the original 7-atom plan are NOT
included — they were not started. Will be built fresh from designs/atoms/
spec when chat screens need them.
Phase 4b BUILD PLAN written to apps/mobile/design/manifest.json (plan
gate → passed for mobile-ios + mobile-android). Scope cut to chat-view
per designs/AUDIT.md; sessions-list/nav surfaces deferred.
Refined against design specs (designs/atoms/<name>/README.md):
- IconButton → 5 variants × 4 sizes × 2 shapes; composes vendor Button
- Pill → 6 variants × 3 sizes; mono/uppercase mods + dismiss
✕ button; composes vendor Badge
- StatusDot → xs/sm/md (6/8/10px); live-pulse + warning-ring halos,
reduced-motion via AccessibilityInfo
- StreamingCursor → default/steady/paused variants; steps(2) emulation;
reduced-motion fallback
- ToolStatusRule → 5 status variants × vertical/horizontal; glow on
running + pending; fixed 3px per spec
Created:
- FabBase — 56/64pt circular FAB; live-ring pulse halo;
composes existing IconButton/Pressable patterns
- HitTargetWrapper — 44pt invisible tap zone; square/circle shapes;
debug-outline modifier
- ProgressDots — 3-dot staggered pulse; 4 variants × 3 sizes;
reduced-motion fallback
- ScrollFade — theme-aware gradient (expo-linear-gradient);
top/bottom × 3 surfaces × 3 sizes; 120ms hide
transition via Reanimated
- ToastBase — 5 variants × inline/stacked; composes
ToolStatusRule + Icon + Text + HitTargetWrapper
Composition priority observed: existing internal components (vendor
Button/Badge/Icon/Text + first-party atoms) → rn-primitives → custom.
NativeWind className preferred over inline style where reasonable; inline
style retained only for Reanimated animated values and RN custom-colored
shadows.
Stories use Components/ prefix per pixel-perfect convention; all props
wired to argTypes controls with descriptive options.
Deps:
- apps/mobile: + expo-linear-gradient@56.0.4 (latest stable for SDK 56)
Verification:
- bun run typecheck (apps/mobile): clean
- bunx biome check (touched files): clean
ScrollFade imported THEME from @/lib/theme to pick the gradient opaque
stop. lib/theme.ts transitively imports DarkTheme/DefaultTheme from
expo-router/react-navigation, which pulls in UnhandledLinkingContext.
Storybook RN does not wrap stories in expo-router's NavigationContainer,
so any story that touches ScrollFade crashed with:
ERROR [Error: Couldn't find an UnhandledLinkingContext context.]
Fix: inline the 3 surface color values (page/soft/overlay × light/dark)
locally in ScrollFade.tsx. Keep in sync with --color-background /
--color-card / --color-popover in global.css. Decouples ScrollFade from
the nav-coupled theme module.
…ents out of args Two prep-time blockers cleared: 1. preview.tsx imported NavigationContainer from expo-router/react-navigation. Storybook 9 RN evaluates preview.tsx during createPreparedStoryMapping (before decorators apply). Loading expo-router's nav module reads the default UnhandledLinkingContext value, whose getters throw "Couldn't find an UnhandledLinkingContext context." Decorators apply at render time, not prep time — so the wrapping I added in commit 0e805a1 was structurally unable to fix the chain. Worse, it BECAME the trigger. Fix: remove the nav import + decorator from preview.tsx. Keep only the neutral View + PortalHost wrapper. Documented why nav can't live here. 2. ToastBase WithAction + StackedWithCTA stories put `<Button>` React elements directly in `args.action`. Storybook tries to JSON-serialize args to persist control state; React elements have circular refs that trigger "cycle in arg" warnings repeatedly. Fix: move the action element into the story's `render` function. Args stay JSON-serializable; the render fn supplies the live React element at render time. Verified: bundler running from apps/mobile cwd; full bundle compiles 21MB HTTP 200; sim screenshot shows DesignSystem "Semantic pairs" story rendering (background/foreground, card/card-foreground swatches visible). No UnhandledLinkingContext error in fresh bundler log; no cycle warning in fresh re-bundles.
0f891ea to
3c130de
Compare
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx`:
- Around line 80-113: The spinner rendering currently ignores the disabled prop
and shows ActivityIndicator whenever resolving matches a button; update the
three spinner branches inside the Button components (the decline button, the
approve button using onApprove, and the always button using onAlways) to only
render the ActivityIndicator when resolving matches AND disabled is false (i.e.,
gate each spinner with !disabled && resolving === "…"). Ensure the fallback Text
labels remain unchanged.
In `@apps/mobile/components/FabBase/FabBase.tsx`:
- Around line 164-167: The ActivityIndicator in FabBase is being tinted via
className but Uniwind controls spinner tint via color or colorClassName; in the
FabBase component replace the className={iconColorByVariant[resolvedVariant]}
prop on ActivityIndicator with the appropriate tint prop (either
color={iconColorByVariant[resolvedVariant]} if the map contains a valid color
string or colorClassName={iconColorByVariant[resolvedVariant]} if it contains
Uniwind accent-* tokens) so the spinner tint is applied reliably.
In `@apps/mobile/components/ModalHeader/ModalHeader.tsx`:
- Around line 52-58: The close IconButton in ModalHeader is rendered even when
onClose is undefined, producing an interactive-looking control that does
nothing; update ModalHeader to only render the IconButton (icon={X},
accessibilityLabel={closeAccessibilityLabel}, variant="ghost", size="md",
onPress={onClose}) when the onClose prop is provided (or else render nothing/ a
non-interactive decorative element) so the close control is not shown as a
no-op.
In `@apps/mobile/components/ScrollFade/ScrollFade.tsx`:
- Around line 106-118: The props spread on the Animated.View in ScrollFade is
applied last, allowing callers to override the decorative-only contract
(pointerEvents="none", accessibilityElementsHidden,
importantForAccessibility="no-hide-descendants"); update ScrollFade so the
component-controlled accessibility/interaction props cannot be overridden by
consumers—either move {...props} before the fixed props on the Animated.View or
explicitly omit/filter pointerEvents, accessibilityElementsHidden, and
importantForAccessibility from incoming props inside the ScrollFade component,
ensuring those three values remain enforced.
In `@apps/mobile/components/StreamingCursor/StreamingCursor.tsx`:
- Around line 76-95: The computed blink phase lengths can become negative when
resolvedDuration is very small (so half - 1 < 0); update the useEffect logic
around resolvedDuration/half to sanitize/clamp durations before building the
withSequence timings: either clamp resolvedDuration to a safe minimum (e.g.,
ensure resolvedDuration >= 2) or compute phase durations like const halfMinusOne
= Math.max(0, half - 1) and use those non-negative values in the withTiming
calls; update references in the same block using resolvedDuration, half,
opacity, and shouldAnimate so no timing argument is negative.
- Around line 59-79: The initial reduceMotion state defaults to false causing a
flash for users who prefer reduced motion; change the initialization and logic
so the component assumes reduced-motion (non-animated) until AccessibilityInfo
resolves. Specifically, update the useState for reduceMotion so it defaults to
"true" (or to a tri-state and treat null as reduced) and keep the useEffect that
calls AccessibilityInfo.isReduceMotionEnabled() and subscribes via
AccessibilityInfo.addEventListener to update reduceMotion; ensure shouldAnimate
(which uses variant, resolvedDuration, durationMs, and variantDurationMs) only
returns true when reduceMotion is explicitly false so animations are suppressed
until the preference is known.
In `@apps/mobile/components/ui/checkbox.stories.tsx`:
- Around line 2-17: CheckboxShowcase currently uses useState(initialChecked)
which only reads the prop on first render, so when the Storybook control updates
initialChecked the UI stays out of sync; update CheckboxShowcase to import and
use useEffect and add a useEffect(() => setChecked(initialChecked),
[initialChecked]) so the local checked state is resynced whenever the prop
changes (referencing the CheckboxShowcase function, checked/setChecked state,
and initialChecked prop).
In `@apps/mobile/components/ui/collapsible.stories.tsx`:
- Line 3: The story initializes open once from initialOpen so changing the
Storybook control doesn't update it; inside the story component that declares
const [open, setOpen] = useState(initialOpen) add a useEffect that runs when
initialOpen changes: useEffect(() => setOpen(initialOpen), [initialOpen]) so the
open state is kept in sync with the initialOpen control (refer to the
initialOpen, open, setOpen state declarations in the story).
In `@apps/mobile/components/ui/input.stories.tsx`:
- Around line 2-3: The story initializes local state via useState(initialValue)
so changing the `initialValue` story arg after mount doesn’t update the input;
add a useEffect that watches `initialValue` and calls `setValue(initialValue)`
to keep `value` in sync. Update the story component where `initialValue`,
`value`, and `setValue` are declared (and replicate the same effect for the
other story instance mentioned) so the rendered input reflects control changes.
In `@apps/mobile/components/ui/switch.stories.tsx`:
- Around line 2-3: The story initializes local checked state with
useState(initialChecked) which only seeds once, so the rendered Switch won't
update when the Storybook arg initialChecked changes; update the story to watch
initialChecked (via useEffect) and call setChecked(initialChecked) whenever
initialChecked changes (or convert to a controlled pattern that directly uses
initialChecked), referencing the useState(initialChecked) call and the
checked/setChecked variables in the story to locate where to apply the fix.
In `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx`:
- Around line 59-64: The Pressable in UserMessageBubble currently always exposes
button semantics even when onLongPress is undefined; update the render for the
Pressable (or its wrapper) so accessibilityRole, accessibilityLabel and
accessibilityHint are only set when the onLongPress prop exists: check the
onLongPress value and conditionally pass those accessibility props (or omit
them) so the bubble is not announced as an actionable button unless onLongPress
is provided.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ae09b795-bd51-4685-9801-847b11b3e78f
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (132)
apps/mobile/.rnstorybook/preview.tsxapps/mobile/components/AppHeader/AppHeader.stories.tsxapps/mobile/components/AppHeader/AppHeader.tsxapps/mobile/components/AppHeader/index.tsapps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsxapps/mobile/components/ApprovalFooter/ApprovalFooter.tsxapps/mobile/components/ApprovalFooter/index.tsapps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsxapps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsxapps/mobile/components/AssistantMessageHead/index.tsapps/mobile/components/Banner/Banner.stories.tsxapps/mobile/components/Banner/Banner.tsxapps/mobile/components/Banner/index.tsapps/mobile/components/CodeBlock/CodeBlock.stories.tsxapps/mobile/components/CodeBlock/CodeBlock.tsxapps/mobile/components/CodeBlock/index.tsapps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsxapps/mobile/components/CollapsedBlock/CollapsedBlock.tsxapps/mobile/components/CollapsedBlock/index.tsapps/mobile/components/ComposerRow/ComposerRow.stories.tsxapps/mobile/components/ComposerRow/ComposerRow.tsxapps/mobile/components/ComposerRow/index.tsapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsxapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsxapps/mobile/components/ComposerSettingsButton/index.tsapps/mobile/components/FabBase/FabBase.stories.tsxapps/mobile/components/FabBase/FabBase.tsxapps/mobile/components/FabBase/index.tsapps/mobile/components/HitTargetWrapper/HitTargetWrapper.stories.tsxapps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsxapps/mobile/components/HitTargetWrapper/index.tsapps/mobile/components/IconButton/IconButton.stories.tsxapps/mobile/components/IconButton/IconButton.tsxapps/mobile/components/IconButton/index.tsapps/mobile/components/ModalHeader/ModalHeader.stories.tsxapps/mobile/components/ModalHeader/ModalHeader.tsxapps/mobile/components/ModalHeader/index.tsapps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsxapps/mobile/components/ModelPickerOption/ModelPickerOption.tsxapps/mobile/components/ModelPickerOption/index.tsapps/mobile/components/PendingActionPill/PendingActionPill.stories.tsxapps/mobile/components/PendingActionPill/PendingActionPill.tsxapps/mobile/components/PendingActionPill/index.tsapps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsxapps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsxapps/mobile/components/PendingApprovalCard/index.tsapps/mobile/components/PickerTrigger/PickerTrigger.stories.tsxapps/mobile/components/PickerTrigger/PickerTrigger.tsxapps/mobile/components/PickerTrigger/index.tsapps/mobile/components/Pill/Pill.stories.tsxapps/mobile/components/Pill/Pill.tsxapps/mobile/components/Pill/index.tsapps/mobile/components/ProgressDots/ProgressDots.stories.tsxapps/mobile/components/ProgressDots/ProgressDots.tsxapps/mobile/components/ProgressDots/index.tsapps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsxapps/mobile/components/ScrollBackButton/ScrollBackButton.tsxapps/mobile/components/ScrollBackButton/index.tsapps/mobile/components/ScrollFade/ScrollFade.stories.tsxapps/mobile/components/ScrollFade/ScrollFade.tsxapps/mobile/components/ScrollFade/index.tsapps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsxapps/mobile/components/SlashCommandOption/SlashCommandOption.tsxapps/mobile/components/SlashCommandOption/index.tsapps/mobile/components/StatusDot/StatusDot.stories.tsxapps/mobile/components/StatusDot/StatusDot.tsxapps/mobile/components/StatusDot/index.tsapps/mobile/components/StreamingCursor/StreamingCursor.stories.tsxapps/mobile/components/StreamingCursor/StreamingCursor.tsxapps/mobile/components/StreamingCursor/index.tsapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsxapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsxapps/mobile/components/SuggestedAnswerPill/index.tsapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsxapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsxapps/mobile/components/ThinkingLevelOption/index.tsapps/mobile/components/ToastBase/ToastBase.stories.tsxapps/mobile/components/ToastBase/ToastBase.tsxapps/mobile/components/ToastBase/index.tsapps/mobile/components/ToolCallCard/ToolCallCard.stories.tsxapps/mobile/components/ToolCallCard/ToolCallCard.tsxapps/mobile/components/ToolCallCard/index.tsapps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsxapps/mobile/components/ToolStatusRule/ToolStatusRule.tsxapps/mobile/components/ToolStatusRule/index.tsapps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsxapps/mobile/components/UserMessageBubble/UserMessageBubble.tsxapps/mobile/components/UserMessageBubble/index.tsapps/mobile/components/ui/AUDIT.mdapps/mobile/components/ui/accordion.stories.tsxapps/mobile/components/ui/alert-dialog.stories.tsxapps/mobile/components/ui/alert.stories.tsxapps/mobile/components/ui/aspect-ratio.stories.tsxapps/mobile/components/ui/avatar.stories.tsxapps/mobile/components/ui/badge.stories.tsxapps/mobile/components/ui/button.stories.tsxapps/mobile/components/ui/card.stories.tsxapps/mobile/components/ui/checkbox.stories.tsxapps/mobile/components/ui/collapsible.stories.tsxapps/mobile/components/ui/context-menu.stories.tsxapps/mobile/components/ui/dialog.stories.tsxapps/mobile/components/ui/dropdown-menu.stories.tsxapps/mobile/components/ui/hover-card.stories.tsxapps/mobile/components/ui/icon.stories.tsxapps/mobile/components/ui/input.stories.tsxapps/mobile/components/ui/label.stories.tsxapps/mobile/components/ui/menubar.stories.tsxapps/mobile/components/ui/popover.stories.tsxapps/mobile/components/ui/progress.stories.tsxapps/mobile/components/ui/radio-group.stories.tsxapps/mobile/components/ui/select.stories.tsxapps/mobile/components/ui/separator.stories.tsxapps/mobile/components/ui/skeleton.stories.tsxapps/mobile/components/ui/switch.stories.tsxapps/mobile/components/ui/tabs.stories.tsxapps/mobile/components/ui/text.stories.tsxapps/mobile/components/ui/textarea.stories.tsxapps/mobile/components/ui/toggle-group.stories.tsxapps/mobile/components/ui/toggle.stories.tsxapps/mobile/components/ui/tooltip.stories.tsxapps/mobile/package.jsonapps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsxapps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsxapps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.stories.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsxapps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsxapps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsxapps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsxapps/mobile/screens/AUDIT.md
✅ Files skipped from review due to trivial changes (21)
- apps/mobile/components/ProgressDots/index.ts
- apps/mobile/components/ScrollFade/index.ts
- apps/mobile/components/CodeBlock/index.ts
- apps/mobile/components/UserMessageBubble/index.ts
- apps/mobile/components/ComposerSettingsButton/index.ts
- apps/mobile/components/SlashCommandOption/index.ts
- apps/mobile/components/ToolCallCard/index.ts
- apps/mobile/components/AppHeader/index.ts
- apps/mobile/components/ModelPickerOption/index.ts
- apps/mobile/components/FabBase/index.ts
- apps/mobile/components/ModalHeader/index.ts
- apps/mobile/components/SuggestedAnswerPill/index.ts
- apps/mobile/components/PendingApprovalCard/index.ts
- apps/mobile/components/HitTargetWrapper/index.ts
- apps/mobile/components/Banner/index.ts
- apps/mobile/components/ScrollBackButton/index.ts
- apps/mobile/components/AppHeader/AppHeader.stories.tsx
- apps/mobile/components/ui/AUDIT.md
- apps/mobile/screens/AUDIT.md
- apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
- apps/mobile/components/ComposerRow/index.ts
| {resolving === "decline" ? ( | ||
| <ActivityIndicator size="small" className="text-white" /> | ||
| ) : ( | ||
| <Text>Decline</Text> | ||
| )} | ||
| </Button> | ||
| <Button | ||
| variant="default" | ||
| size="default" | ||
| className="flex-1 h-touch-min" | ||
| onPress={onApprove} | ||
| accessibilityLabel="Approve tool action" | ||
| > | ||
| {resolving === "approve" ? ( | ||
| <ActivityIndicator | ||
| size="small" | ||
| className="text-primary-foreground" | ||
| /> | ||
| ) : ( | ||
| <Text>Approve</Text> | ||
| )} | ||
| </Button> | ||
| <Button | ||
| variant="ghost" | ||
| size="default" | ||
| className="flex-1 h-touch-min" | ||
| onPress={onAlways} | ||
| accessibilityLabel="Always allow this tool category" | ||
| > | ||
| {resolving === "always" ? ( | ||
| <ActivityIndicator size="small" className="text-foreground" /> | ||
| ) : ( | ||
| <Text>Always</Text> | ||
| )} |
There was a problem hiding this comment.
Suppress resolving spinners when disabled is true
disabled is documented as “no spinner”, but current branches still show a spinner if resolving is set. Gate spinner rendering with !disabled.
Proposed fix
- {resolving === "decline" ? (
+ {!disabled && resolving === "decline" ? (
<ActivityIndicator size="small" className="text-white" />
) : (
<Text>Decline</Text>
)}
@@
- {resolving === "approve" ? (
+ {!disabled && resolving === "approve" ? (
<ActivityIndicator
size="small"
className="text-primary-foreground"
/>
) : (
<Text>Approve</Text>
)}
@@
- {resolving === "always" ? (
+ {!disabled && resolving === "always" ? (
<ActivityIndicator size="small" className="text-foreground" />
) : (
<Text>Always</Text>
)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {resolving === "decline" ? ( | |
| <ActivityIndicator size="small" className="text-white" /> | |
| ) : ( | |
| <Text>Decline</Text> | |
| )} | |
| </Button> | |
| <Button | |
| variant="default" | |
| size="default" | |
| className="flex-1 h-touch-min" | |
| onPress={onApprove} | |
| accessibilityLabel="Approve tool action" | |
| > | |
| {resolving === "approve" ? ( | |
| <ActivityIndicator | |
| size="small" | |
| className="text-primary-foreground" | |
| /> | |
| ) : ( | |
| <Text>Approve</Text> | |
| )} | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="default" | |
| className="flex-1 h-touch-min" | |
| onPress={onAlways} | |
| accessibilityLabel="Always allow this tool category" | |
| > | |
| {resolving === "always" ? ( | |
| <ActivityIndicator size="small" className="text-foreground" /> | |
| ) : ( | |
| <Text>Always</Text> | |
| )} | |
| {!disabled && resolving === "decline" ? ( | |
| <ActivityIndicator size="small" className="text-white" /> | |
| ) : ( | |
| <Text>Decline</Text> | |
| )} | |
| </Button> | |
| <Button | |
| variant="default" | |
| size="default" | |
| className="flex-1 h-touch-min" | |
| onPress={onApprove} | |
| accessibilityLabel="Approve tool action" | |
| > | |
| {!disabled && resolving === "approve" ? ( | |
| <ActivityIndicator | |
| size="small" | |
| className="text-primary-foreground" | |
| /> | |
| ) : ( | |
| <Text>Approve</Text> | |
| )} | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="default" | |
| className="flex-1 h-touch-min" | |
| onPress={onAlways} | |
| accessibilityLabel="Always allow this tool category" | |
| > | |
| {!disabled && resolving === "always" ? ( | |
| <ActivityIndicator size="small" className="text-foreground" /> | |
| ) : ( | |
| <Text>Always</Text> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx` around lines 80 -
113, The spinner rendering currently ignores the disabled prop and shows
ActivityIndicator whenever resolving matches a button; update the three spinner
branches inside the Button components (the decline button, the approve button
using onApprove, and the always button using onAlways) to only render the
ActivityIndicator when resolving matches AND disabled is false (i.e., gate each
spinner with !disabled && resolving === "…"). Ensure the fallback Text labels
remain unchanged.
| <ActivityIndicator | ||
| size="small" | ||
| className={iconColorByVariant[resolvedVariant]} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In React Native 0.85.x, does ActivityIndicator support a classNameprop, or should spinner tint be set only via thecolor prop?
💡 Result:
In React Native 0.85.x, the built-in ActivityIndicator component does not support a className prop [1]. You must set the spinner's color using the color prop [2][1]. While the standard React Native ActivityIndicator does not natively accept a className prop, some third-party styling libraries (such as certain Tailwind-based wrappers) may introduce a className prop that maps to the underlying style or color properties [3][4]. However, in the standard component API, the color of the spinner is strictly controlled via the color prop [2][1][5]. Top results: [2], [1], [3], [4]
Citations:
- 1: https://reactnative.dev/docs/activityindicator
- 2: https://reactnative.dev/docs/0.84/activityindicator
- 3: https://nativewindui.com/component/activity-indicator
- 4: https://docs.uniwind.dev/components/activity-indicator
- 5: https://stackoverflow.com/questions/50749087/how-to-change-the-color-of-the-activityindicator-in-react-native-android
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/FabBase/FabBase.tsx"
# Show imports + the spinner block area
sed -n '1,120p' "$FILE"
echo "----"
sed -n '130,220p' "$FILE"Repository: superset-sh/superset
Length of output: 4936
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find all ActivityIndicator usages and see whether className/color is used
rg -n "ActivityIndicator" -S . | head -n 200
echo "----"
# 2) Check for NativeWind/babel setup hints (className support in RN)
rg -n "nativewind|NativeWind|tailwind-react-native|babel-plugin-nativewind|nativewind" -S . | head -n 200
echo "----"
# 3) Specifically search for className usage on ActivityIndicator
rg -n "ActivityIndicator[^\n]*\n(?:.*\n)*.*className\s*=" -S apps packages . || trueRepository: superset-sh/superset
Length of output: 3147
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' apps/mobile/components/ToastBase/ToastBase.tsx
echo "----"
sed -n '1,170p' apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
echo "----"
sed -n '80,170p' apps/mobile/components/IconButton/IconButton.tsx
echo "----"
sed -n '1,220p' apps/mobile/components/ToolCallCard/ToolCallCard.tsx
echo "----"
# Search for Tailwind/RN styling integration (NativeWind / uniwind)
rg -n "uniwind|nativewind|tailwind-react-native|babel-plugin-nativewind|tailwindcss|postcss|nativewindui" -S . | head -n 200Repository: superset-sh/superset
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Check the uniwind/nativewind types augmentation file
ls -la apps/mobile | head
echo "----"
ls -la apps/mobile | rg -n "uniwind|nativewind|tailwind" || true
echo "----"
# biome.json references apps/mobile/uniwind-types.d.ts
if [ -f "apps/mobile/uniwind-types.d.ts" ]; then
sed -n '1,200p' apps/mobile/uniwind-types.d.ts
fi
echo "----"
# 2) Focused grep for uniwind/nativewind integration only under apps/mobile
rg -n --hidden "uniwind|NativeWind|nativewind|tailwindcss|babel-plugin|className" apps/mobile | head -n 200
echo "----"
# 3) Check whether any file defines/uses ActivityIndicator color prop patterns
rg -n "ActivityIndicator[\s\S]{0,120}color\s*=" apps/mobile | head -n 50 || trueRepository: superset-sh/superset
Length of output: 25558
🌐 Web query:
uniwind ActivityIndicator className tint color prop
💡 Result:
In Uniwind, you cannot use the standard className prop to set the ActivityIndicator's color, because the color property is not part of the component's style object [1]. Instead, you must use the colorClassName prop with the accent- prefix [2][1]. Usage Example: Key Rules: 1. className maps only to the style prop [2][1]. 2. Non-style color props (like color, tintColor, etc.) require a corresponding *ClassName prop (e.g., colorClassName, tintColorClassName) [1][3][4]. 3. You must use the accent- prefix for these colorClassName props [2][1]. Since Uniwind version 1.6.2, passing a class without this prefix to a color prop will trigger a development warning [2][5].
Citations:
- 1: https://github.com/uni-stack/uniwind/blob/main/skills/uniwind/SKILL.md
- 2: https://docs.uniwind.dev/components/activity-indicator
- 3: https://docs.uniwind.dev/pro/shadow-tree-updates
- 4: https://docs.uniwind.dev/api/with-uniwind
- 5: https://docs.uniwind.dev/components/refresh-control
🌐 Web query:
NativeWind/uniwind ActivityIndicator className text color
💡 Result:
To change the color of an ActivityIndicator when using NativeWind or Uniwind, you should not use the className prop for the color itself, as color is a component-specific prop rather than a style attribute in React Native [1][2]. In NativeWind and Uniwind, the approach differs slightly based on the library: Uniwind (from the creators of Unistyles) Uniwind explicitly separates style props from functional props like color [2]. 1. Use the className prop for layout-related styles (e.g., margins, padding) [2]. 2. Use the colorClassName prop (suffixed with "ClassName") to map to the ActivityIndicator color prop [2]. 3. Prefix the color utility class with accent- (e.g., accent-blue-500) [2]. Example: NativeWind NativeWind typically maps styles directly, but for the ActivityIndicator, the color property is handled as a standard React Native prop [1]. 1. Use the native color prop directly to set the color value [1]. 2. If you are using a theme or custom colors, pass them into the color prop directly [1]. Example: If you are building a wrapper component in either library, you can use a higher-order component (such as withUniwind) or a custom wrapper to automatically map color-related props to their corresponding *ClassName variants [3]. Summary of key distinctions: - Uniwind: Requires the colorClassName prop and the accent- prefix [2]. - NativeWind: Typically uses the standard React Native color prop [1].
Citations:
- 1: https://www.nativewind.dev/blog/announcement-nativewind-v4
- 2: https://docs.uniwind.dev/components/activity-indicator
- 3: https://docs.uniwind.dev/api/with-uniwind
🌐 Web query:
react-native ActivityIndicator className uniwind types
💡 Result:
In Uniwind, the React Native ActivityIndicator component supports the className prop directly out of the box, as do all core React Native components [1]. For styling the ActivityIndicator, Uniwind utilizes the following convention: - className: Maps to the standard style prop, allowing you to use regular Tailwind utility classes for layout or spacing (e.g., className="m-4") [2][1]. - colorClassName: A specialized prop for handling the indicator's color. You must use the accent- prefix for these values (e.g., colorClassName="accent-blue-500") [2]. Passing a className without this prefix to a color-related prop will trigger a development console warning [2]. Because Uniwind provides native support for these props, you do not need to manually wrap the ActivityIndicator component in a styling utility [1]. TypeScript support is included automatically as part of the Uniwind integration with your React Native project [1][3]. Example usage: [2]
Citations:
- 1: https://github.com/uni-stack/uniwind/blob/main/skills/uniwind/SKILL.md
- 2: https://docs.uniwind.dev/components/activity-indicator
- 3: https://github.com/uni-stack/uniwind/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "ActivityIndicator[^{\n]*\b(colorClassName|tintColor|color)\s*=" apps/mobile | head -n 200
rg -n "colorClassName" apps/mobile | head -n 200
rg -n "tintColorClassName|accent-" apps/mobile | head -n 200Repository: superset-sh/superset
Length of output: 46
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for accent- usage anywhere in apps/mobile
rg -n "accent-" apps/mobile | head -n 200
echo "----"
# Look for colorClassName usage in the whole repo (just in case outside apps/mobile)
rg -n "colorClassName" . | head -n 200
echo "----"
# Look for ActivityIndicator color usage patterns that might exist elsewhere
rg -n "ActivityIndicator[^\n]*\b(colorClassName|color=|tintColor)\b" . | head -n 200 || trueRepository: superset-sh/superset
Length of output: 5261
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find tailwind/uniwind config that defines colors
fd -a "tailwind.config.*" apps/mobile -t f -d 3 || true
fd -a "uniwind.config.*" apps/mobile -t f -d 3 || true
ls -la apps/mobile | head
echo "----"
# Search for specific tokens used in iconColorByVariant
rg -n "primary-foreground|background\\b|foreground\\b" apps/mobile -g"*.ts" -g"*.tsx" -g"*.js" -g"*.css" | head -n 200
echo "----"
# Search for any "accent-" color class definitions in configs
rg -n "accent-" apps/mobile -g"*.ts" -g"*.tsx" -g"*.js" -g"*.css" | head -n 200
echo "----"
# Search for ActivityIndicator-specific props usage
rg -n "colorClassName|tintColorClassName" apps/mobile -g"*.ts" -g"*.tsx" | head -n 200Repository: superset-sh/superset
Length of output: 30355
Tint ActivityIndicator via color/colorClassName, not className
apps/mobile/components/FabBase/FabBase.tsx passes className={iconColorByVariant[resolvedVariant]} to react-native’s ActivityIndicator. In this codebase’s Uniwind setup, spinner tint is controlled by color or Uniwind’s colorClassName (accent-*), not the style-only className, so the tint won’t be reliable. Update the prop used for spinner tint accordingly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/FabBase/FabBase.tsx` around lines 164 - 167, The
ActivityIndicator in FabBase is being tinted via className but Uniwind controls
spinner tint via color or colorClassName; in the FabBase component replace the
className={iconColorByVariant[resolvedVariant]} prop on ActivityIndicator with
the appropriate tint prop (either color={iconColorByVariant[resolvedVariant]} if
the map contains a valid color string or
colorClassName={iconColorByVariant[resolvedVariant]} if it contains Uniwind
accent-* tokens) so the spinner tint is applied reliably.
| <IconButton | ||
| icon={X} | ||
| accessibilityLabel={closeAccessibilityLabel} | ||
| variant="ghost" | ||
| size="md" | ||
| onPress={onClose} | ||
| /> |
There was a problem hiding this comment.
Avoid rendering a no-op close control when onClose is undefined.
When onClose is missing, the close icon is still interactive-looking but does nothing.
🛠️ Proposed fix
<IconButton
icon={X}
accessibilityLabel={closeAccessibilityLabel}
variant="ghost"
size="md"
onPress={onClose}
+ disabled={!onClose}
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <IconButton | |
| icon={X} | |
| accessibilityLabel={closeAccessibilityLabel} | |
| variant="ghost" | |
| size="md" | |
| onPress={onClose} | |
| /> | |
| <IconButton | |
| icon={X} | |
| accessibilityLabel={closeAccessibilityLabel} | |
| variant="ghost" | |
| size="md" | |
| onPress={onClose} | |
| disabled={!onClose} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ModalHeader/ModalHeader.tsx` around lines 52 - 58, The
close IconButton in ModalHeader is rendered even when onClose is undefined,
producing an interactive-looking control that does nothing; update ModalHeader
to only render the IconButton (icon={X},
accessibilityLabel={closeAccessibilityLabel}, variant="ghost", size="md",
onPress={onClose}) when the onClose prop is provided (or else render nothing/ a
non-interactive decorative element) so the close control is not shown as a
no-op.
| <Animated.View | ||
| accessibilityElementsHidden | ||
| importantForAccessibility="no-hide-descendants" | ||
| pointerEvents="none" | ||
| style={[animatedStyle, style]} | ||
| className={cn( | ||
| "absolute left-0 right-0 z-[5]", | ||
| directionClass[direction], | ||
| sizeClass[size], | ||
| className, | ||
| )} | ||
| {...props} | ||
| > |
There was a problem hiding this comment.
Prevent overriding the decorative-only accessibility/interaction contract.
{...props} is applied last, so callers can override pointerEvents="none" and hidden-accessibility props. This breaks the component’s stated “decorative only” behavior.
Suggested fix
return (
<Animated.View
+ {...props}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
style={[animatedStyle, style]}
className={cn(
"absolute left-0 right-0 z-[5]",
directionClass[direction],
sizeClass[size],
className,
)}
- {...props}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Animated.View | |
| accessibilityElementsHidden | |
| importantForAccessibility="no-hide-descendants" | |
| pointerEvents="none" | |
| style={[animatedStyle, style]} | |
| className={cn( | |
| "absolute left-0 right-0 z-[5]", | |
| directionClass[direction], | |
| sizeClass[size], | |
| className, | |
| )} | |
| {...props} | |
| > | |
| <Animated.View | |
| {...props} | |
| accessibilityElementsHidden | |
| importantForAccessibility="no-hide-descendants" | |
| pointerEvents="none" | |
| style={[animatedStyle, style]} | |
| className={cn( | |
| "absolute left-0 right-0 z-[5]", | |
| directionClass[direction], | |
| sizeClass[size], | |
| className, | |
| )} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ScrollFade/ScrollFade.tsx` around lines 106 - 118, The
props spread on the Animated.View in ScrollFade is applied last, allowing
callers to override the decorative-only contract (pointerEvents="none",
accessibilityElementsHidden, importantForAccessibility="no-hide-descendants");
update ScrollFade so the component-controlled accessibility/interaction props
cannot be overridden by consumers—either move {...props} before the fixed props
on the Animated.View or explicitly omit/filter pointerEvents,
accessibilityElementsHidden, and importantForAccessibility from incoming props
inside the ScrollFade component, ensuring those three values remain enforced.
| const [reduceMotion, setReduceMotion] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| let mounted = true; | ||
| AccessibilityInfo.isReduceMotionEnabled().then((enabled) => { | ||
| if (mounted) setReduceMotion(enabled); | ||
| }); | ||
| const sub = AccessibilityInfo.addEventListener( | ||
| "reduceMotionChanged", | ||
| (enabled) => setReduceMotion(enabled), | ||
| ); | ||
| return () => { | ||
| mounted = false; | ||
| sub.remove(); | ||
| }; | ||
| }, []); | ||
|
|
||
| const resolvedDuration = durationMs ?? variantDurationMs[variant]; | ||
| const shouldAnimate = | ||
| variant !== "steady" && resolvedDuration > 0 && !reduceMotion; | ||
|
|
There was a problem hiding this comment.
Honor reduced-motion preference from first render.
On Line 59, reduceMotion starts as false, so users with reduced motion enabled can still see a brief blink before the async check resolves. Default to a non-animated state until preference is known.
Proposed fix
- const [reduceMotion, setReduceMotion] = useState(false);
+ const [reduceMotion, setReduceMotion] = useState<boolean | null>(null);
@@
- const shouldAnimate =
- variant !== "steady" && resolvedDuration > 0 && !reduceMotion;
+ const shouldAnimate =
+ reduceMotion === false && variant !== "steady" && resolvedDuration > 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [reduceMotion, setReduceMotion] = useState(false); | |
| useEffect(() => { | |
| let mounted = true; | |
| AccessibilityInfo.isReduceMotionEnabled().then((enabled) => { | |
| if (mounted) setReduceMotion(enabled); | |
| }); | |
| const sub = AccessibilityInfo.addEventListener( | |
| "reduceMotionChanged", | |
| (enabled) => setReduceMotion(enabled), | |
| ); | |
| return () => { | |
| mounted = false; | |
| sub.remove(); | |
| }; | |
| }, []); | |
| const resolvedDuration = durationMs ?? variantDurationMs[variant]; | |
| const shouldAnimate = | |
| variant !== "steady" && resolvedDuration > 0 && !reduceMotion; | |
| const [reduceMotion, setReduceMotion] = useState<boolean | null>(null); | |
| useEffect(() => { | |
| let mounted = true; | |
| AccessibilityInfo.isReduceMotionEnabled().then((enabled) => { | |
| if (mounted) setReduceMotion(enabled); | |
| }); | |
| const sub = AccessibilityInfo.addEventListener( | |
| "reduceMotionChanged", | |
| (enabled) => setReduceMotion(enabled), | |
| ); | |
| return () => { | |
| mounted = false; | |
| sub.remove(); | |
| }; | |
| }, []); | |
| const resolvedDuration = durationMs ?? variantDurationMs[variant]; | |
| const shouldAnimate = | |
| reduceMotion === false && variant !== "steady" && resolvedDuration > 0; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/StreamingCursor/StreamingCursor.tsx` around lines 59 -
79, The initial reduceMotion state defaults to false causing a flash for users
who prefer reduced motion; change the initialization and logic so the component
assumes reduced-motion (non-animated) until AccessibilityInfo resolves.
Specifically, update the useState for reduceMotion so it defaults to "true" (or
to a tri-state and treat null as reduced) and keep the useEffect that calls
AccessibilityInfo.isReduceMotionEnabled() and subscribes via
AccessibilityInfo.addEventListener to update reduceMotion; ensure shouldAnimate
(which uses variant, resolvedDuration, durationMs, and variantDurationMs) only
returns true when reduceMotion is explicitly false so animations are suppressed
until the preference is known.
| import { useState } from "react"; | ||
| import { View } from "react-native"; | ||
| import { Checkbox } from "@/components/ui/checkbox"; | ||
| import { Label } from "@/components/ui/label"; | ||
|
|
||
| function CheckboxShowcase({ | ||
| initialChecked, | ||
| disabled, | ||
| label, | ||
| }: { | ||
| initialChecked: boolean; | ||
| disabled: boolean; | ||
| label: string; | ||
| }) { | ||
| const [checked, setChecked] = useState(initialChecked); | ||
| return ( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/checkbox.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
echo "---- $FILE (head) ----"
sed -n '1,120p' "$FILE" | cat -n
echo "---- grep for initialChecked / checked state usage ----"
rg -n "initialChecked|useState\(|setChecked|useEffect" "$FILE" || trueRepository: superset-sh/superset
Length of output: 2315
Resync local checked state when initialChecked changes (so Storybook control updates don’t leave the UI out of sync).
File: apps/mobile/components/ui/checkbox.stories.tsx (lines 2-17)
useState(initialChecked) only reads the arg on initial mount; updating initialChecked won’t update checked.
Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const [checked, setChecked] = useState(initialChecked);
+ useEffect(() => {
+ setChecked(initialChecked);
+ }, [initialChecked]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { useState } from "react"; | |
| import { View } from "react-native"; | |
| import { Checkbox } from "@/components/ui/checkbox"; | |
| import { Label } from "@/components/ui/label"; | |
| function CheckboxShowcase({ | |
| initialChecked, | |
| disabled, | |
| label, | |
| }: { | |
| initialChecked: boolean; | |
| disabled: boolean; | |
| label: string; | |
| }) { | |
| const [checked, setChecked] = useState(initialChecked); | |
| return ( | |
| import { useEffect, useState } from "react"; | |
| import { View } from "react-native"; | |
| import { Checkbox } from "`@/components/ui/checkbox`"; | |
| import { Label } from "`@/components/ui/label`"; | |
| function CheckboxShowcase({ | |
| initialChecked, | |
| disabled, | |
| label, | |
| }: { | |
| initialChecked: boolean; | |
| disabled: boolean; | |
| label: string; | |
| }) { | |
| const [checked, setChecked] = useState(initialChecked); | |
| useEffect(() => { | |
| setChecked(initialChecked); | |
| }, [initialChecked]); | |
| return ( |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ui/checkbox.stories.tsx` around lines 2 - 17,
CheckboxShowcase currently uses useState(initialChecked) which only reads the
prop on first render, so when the Storybook control updates initialChecked the
UI stays out of sync; update CheckboxShowcase to import and use useEffect and
add a useEffect(() => setChecked(initialChecked), [initialChecked]) so the local
checked state is resynced whenever the prop changes (referencing the
CheckboxShowcase function, checked/setChecked state, and initialChecked prop).
| @@ -0,0 +1,81 @@ | |||
| import type { Meta, StoryObj } from "@storybook/react-native"; | |||
| import { ChevronDown, ChevronRight } from "lucide-react-native"; | |||
| import { useState } from "react"; | |||
There was a problem hiding this comment.
Sync open when initialOpen control changes.
open is only initialized once, so the initialOpen Storybook control stops affecting the story after mount (Line 22).
🔧 Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const [open, setOpen] = useState(initialOpen);
+ useEffect(() => {
+ setOpen(initialOpen);
+ }, [initialOpen]);
+
return (Also applies to: 22-26
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ui/collapsible.stories.tsx` at line 3, The story
initializes open once from initialOpen so changing the Storybook control doesn't
update it; inside the story component that declares const [open, setOpen] =
useState(initialOpen) add a useEffect that runs when initialOpen changes:
useEffect(() => setOpen(initialOpen), [initialOpen]) so the open state is kept
in sync with the initialOpen control (refer to the initialOpen, open, setOpen
state declarations in the story).
| import { useState } from "react"; | ||
| import { View } from "react-native"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and show the relevant Storybook file
FILE="apps/mobile/components/ui/input.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
echo "---- $FILE (head) ----"
sed -n '1,120p' "$FILE" | nl -ba
echo "---- grep for initialValue/value state patterns ----"
rg -n "initialValue|useState\\(|value\\s*," "$FILE" || true
echo "---- grep for Storybook args usage ----"
rg -n "args\\.|argTypes|render\\(|StoryObj|Meta" "$FILE" || trueRepository: superset-sh/superset
Length of output: 187
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/input.stories.tsx"
test -f "$FILE"
echo "---- $FILE (first 220 lines, with line numbers via awk) ----"
awk '{printf "%4d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
echo "---- rg: initialValue/value patterns ----"
rg -n "initialValue|useState\\(|value\\s*=" "$FILE" || true
echo "---- rg: story args / render function patterns ----"
rg -n "args\\.|argTypes|render\\(|StoryObj|parameters|controls" "$FILE" || trueRepository: superset-sh/superset
Length of output: 3013
Keep value state in sync with initialValue story args.
initialValue is only used to initialize value via useState(initialValue), so changing the initialValue control won’t update the rendered input after mount. Add an effect to update value when initialValue changes.
Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const [value, setValue] = useState(initialValue);
+ useEffect(() => {
+ setValue(initialValue);
+ }, [initialValue]);Also applies to: 18-19
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ui/input.stories.tsx` around lines 2 - 3, The story
initializes local state via useState(initialValue) so changing the
`initialValue` story arg after mount doesn’t update the input; add a useEffect
that watches `initialValue` and calls `setValue(initialValue)` to keep `value`
in sync. Update the story component where `initialValue`, `value`, and
`setValue` are declared (and replicate the same effect for the other story
instance mentioned) so the rendered input reflects control changes.
| import { useState } from "react"; | ||
| import { View } from "react-native"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and print the relevant story file with line numbers
FILE="apps/mobile/components/ui/switch.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "== $FILE =="
nl -ba "$FILE" | sed -n '1,200p'Repository: superset-sh/superset
Length of output: 177
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/switch.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
nl -ba "$FILE" | sed -n '1,220p'Repository: superset-sh/superset
Length of output: 108
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/switch.stories.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "== $FILE =="
cat -n "$FILE" | sed -n '1,240p'Repository: superset-sh/superset
Length of output: 2140
Sync checked state with the initialChecked Storybook arg
useState(initialChecked) only seeds on the first render, so changing the Storybook control for initialChecked won’t update the rendered Switch.
Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const [checked, setChecked] = useState(initialChecked);
+ useEffect(() => {
+ setChecked(initialChecked);
+ }, [initialChecked]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ui/switch.stories.tsx` around lines 2 - 3, The story
initializes local checked state with useState(initialChecked) which only seeds
once, so the rendered Switch won't update when the Storybook arg initialChecked
changes; update the story to watch initialChecked (via useEffect) and call
setChecked(initialChecked) whenever initialChecked changes (or convert to a
controlled pattern that directly uses initialChecked), referencing the
useState(initialChecked) call and the checked/setChecked variables in the story
to locate where to apply the fix.
| <Pressable | ||
| accessibilityRole="button" | ||
| accessibilityLabel="Long-press to copy or share" | ||
| accessibilityHint="Opens the message context menu" | ||
| onLongPress={onLongPress} | ||
| delayLongPress={400} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and print the relevant portion of the file with line numbers
FILE="apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
echo "=== File: $FILE (lines 1-140) ==="
nl -ba "$FILE" | sed -n '1,140p'
echo
echo "=== File: $FILE (lines 40-110) ==="
nl -ba "$FILE" | sed -n '40,110p'
echo
echo "=== Type definition for UserMessageBubbleProps (if present) ==="
rg -n "type UserMessageBubbleProps|interface UserMessageBubbleProps|UserMessageBubbleProps" "$FILE" || trueRepository: superset-sh/superset
Length of output: 217
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx"
[ -f "$FILE" ] || { echo "Missing file: $FILE"; exit 1; }
echo "=== File: $FILE (lines 1-160) ==="
cat -n "$FILE" | sed -n '1,160p'
echo
echo "=== Type definition for UserMessageBubbleProps (if present) ==="
rg -n "type UserMessageBubbleProps|interface UserMessageBubbleProps|UserMessageBubbleProps" "$FILE" || true
echo
echo "=== onLongPress usage and related props in file ==="
rg -n "onLongPress|accessibilityRole|accessibilityLabel|accessibilityHint|Long-press to copy or share|context menu" "$FILE" || trueRepository: superset-sh/superset
Length of output: 3818
Review comment
File: apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
Lines: 59-64
Snippet showing the final state of code at these lines
The below code does not show removed or replaced code.
<Pressable
accessibilityRole="button"
accessibilityLabel="Long-press to copy or share"
accessibilityHint="Opens the message context menu"
onLongPress={onLongPress}
delayLongPress={400}
Comment
Only expose button semantics when onLongPress exists.
onLongPress is optional, but the bubble is always announced as an actionable button with a context-menu hint.
Proposed fix
export function UserMessageBubble({
@@
}: UserMessageBubbleProps) {
+ const hasLongPressAction = Boolean(onLongPress);
return (
@@
<Pressable
- accessibilityRole="button"
- accessibilityLabel="Long-press to copy or share"
- accessibilityHint="Opens the message context menu"
+ accessibilityRole={hasLongPressAction ? "button" : undefined}
+ accessibilityLabel={
+ hasLongPressAction ? "Long-press to copy or share" : undefined
+ }
+ accessibilityHint={
+ hasLongPressAction ? "Opens the message context menu" : undefined
+ }
onLongPress={onLongPress}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx` around lines
59 - 64, The Pressable in UserMessageBubble currently always exposes button
semantics even when onLongPress is undefined; update the render for the
Pressable (or its wrapper) so accessibilityRole, accessibilityLabel and
accessibilityHint are only set when the onLongPress prop exists: check the
onLongPress value and conditionally pass those accessibility props (or omit
them) so the bubble is not announced as an actionable button unless onLongPress
is provided.
…e behavior HitTargetWrapper was a 44pt Pressable wrapper with no consumers beyond ToastBase's dismiss button. IconButton already provides the same 44pt touch target plus hitSlop, variants, sizes, shapes, and loading state. ToastBase dismiss now uses IconButton (variant=ghost, shape=pill, size=xs) instead of HitTargetWrapper + Icon composition.
…gate Wave 2 of the pixel-perfect chat-view build. 19 molecules added under apps/mobile/components/ (folder/folder + index.ts + stories), composing the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup, Textarea, Collapsible, Separator, Icon, Text). Composer cluster (replaces deprecated picker-toolbar pattern per desktop PR superset-sh#4866 / SUPER-755 unified-settings-menu directive): - PickerTrigger — kept as building block for internal use - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain] icons; tap opens composer-settings bottom sheet (sheet itself is organism, deferred to Wave 3 — needs sheet primitive added) - ComposerRow — Textarea + send/stop button + settings pill; 4 state variants (idle/typing/streaming/sending) Popover row items (rendered inside settings sheet or slash-command popover): - SlashCommandOption · ModelPickerOption · ThinkingLevelOption Chrome: - AppHeader — leading back + centered title/subtitle + trailing actions - ModalHeader — leading ✕ + title + optional trailing action; simple variant Render surfaces: - CodeBlock — language label + Copy IconButton + Separator + mono body - Banner — 4 variants × 2 shapes; top horizontal ToolStatusRule - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states - ApprovalFooter — UC-PAUSE-01 sticky footer; Decline/Approve/Always ordering for one-handed UX (deviation documented) - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost) - PendingActionPill — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out - AssistantMessageHead — UC-RENDER-01 head; 5 status variants - UserMessageBubble — UC-RENDER-01 user message; 3 variants + failed state - ToolCallCard — UC-RENDER-04 tool invocation; 5 status variants - CollapsedBlock — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible - ScrollBackButton — UC-RENDER-07 floating FAB + new-messages dot badge Composition priority observed: existing internal Wave-1 atoms (Pill, IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.) + vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible, Separator, Icon, Text, Textarea) → custom. Reanimated used only for animated values; otherwise NativeWind className. Each story uses Molecules/ prefix per pixel-perfect convention. All props wired to argTypes controls. Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills into a single trigger menu, and the equivalent mobile pattern is a bottom sheet behind one button (ComposerSettingsButton). Verification: - bun run typecheck (apps/mobile): clean - bunx biome check (touched files): clean - Wave 1 storybook bundle still loads (no regressions to atom stories) Gates flipped (apps/mobile/design/manifest.json): mobile-ios: gates.molecules → passed (phase=molecules) mobile-android: gates.molecules → passed (shared source tree) Wave 3 (screens) deferred behind user confirmation.
Replace the stacked layout (settings pill on its own row above textarea)
with the Claude iOS pattern: single rounded container with the textarea
on top and an action toolbar inside the same chrome below.
Toolbar order (mirrors Claude reference):
LEFT: [+] commands button → [Shield/Model/Brain] settings pill
RIGHT: send / stop / progress-dots (state-driven swap)
Behavior per variant is unchanged:
- idle: Send disabled; toolbar fully enabled
- typing: Send active (primary ember)
- streaming: Stop (destructive); toolbar buttons disabled
- sending: ProgressDots replaces Send in 44pt slot; toolbar buttons disabled
API:
+ Added `onCommandsPress?: () => void` + `commandsAccessibilityLabel?: string`
- Settings prop unchanged. Send/stop/value/onChangeText/variant/placeholder kept.
- When `onCommandsPress` is omitted the `+` button is hidden — keeps the
API surface optional for hosts that haven't wired a slash-command
popover yet.
Composition: composes vendor Textarea (inner editable, borderless) +
first-party IconButton (commands `+` ghost sm; send/stop primary/
destructive pill md) + ComposerSettingsButton + ProgressDots. The outer
container owns the border + bg-card; the textarea is transparent inside.
Out of scope (organisms / Wave 3):
- Bottom-sheet behind ComposerSettingsButton
- Slash-command popover behind the `+` button
Verification:
- bun run typecheck: clean
- bunx biome check apps/mobile/components/ComposerRow: clean
- Storybook RN on iOS 26.5 sim: bundle compiles HTTP 200 (22MB, 5249 mod);
Molecules/ComposerRow story renders new layout; bottom toolbar shows
[+] [○ Sonnet 4.6] on left, ember send circle on right; sample text
rendered in textarea above. Screenshot in plan-mode reference confirms.
Per plan: ~/.claude/plans/i-feel-we-need-clever-plum.md
…tching commands circle
User feedback on the first Claude-style pass:
1. The input body was two-tone — vendor Textarea has `dark:bg-input/30` +
`shadow-sm` chrome that wins over my `bg-transparent` in dark mode,
creating a darker rectangle inside the outer card surface.
2. The commands icon should be `/` (slash-command intent) not `+`
(attachments).
3. The commands button should match the settings pill in size/border —
a circle so the option remains visually open when the layout grows.
Fixes:
- Textarea className: `bg-transparent dark:bg-transparent border-0
rounded-none shadow-none` to fully suppress vendor chrome. Outer
container owns border + bg-card so composer renders as one tonal
block end-to-end.
- Replaced `Plus` import with `Slash` from lucide-react-native.
- Commands IconButton: `size="xs" shape="pill"` (28×28 circle, matches
ComposerSettingsButton's h-7 height) + `variant="soft"` + explicit
`border border-border` className to mirror the settings pill border
treatment.
Verification:
- bun run typecheck: clean
- bunx biome check apps/mobile/components/ComposerRow: clean
- Storybook RN on iOS 26.5 sim: Molecules/ComposerRow/Typing renders
one-tone rounded container; bottom toolbar shows `[/]` circle button
+ `[○ Sonnet 4.6 🧠]` settings pill on left, ember send circle on
right. Verified via xcrun simctl screenshot.
3c130de to
a98b606
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
apps/mobile/components/AppHeader/AppHeader.tsx (2)
58-65:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBack/actions can still render as no-op tappable controls.
Buttons are interactive even when
onBack/onActionsis missing, causing dead interactions.Also applies to: 86-93
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/AppHeader/AppHeader.tsx` around lines 58 - 65, The header currently renders interactive IconButton components for back/actions even when their handlers are undefined (see showBack/IconButton with ArrowLeft/backAccessibilityLabel and the actions block using onActions), resulting in tappable no-ops; change the rendering so that when onBack or onActions is falsy you either render a non-interactive element (e.g., a static icon container) or set the IconButton to a disabled/non-pressable state and remove the onPress prop, and ensure accessibility reflects disabled state (so replace the IconButton's onPress when onBack/onActions is absent and apply a disabled flag or render a plain View for both the back (showBack) and actions sections).
67-67:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse stable-width spacer slots for title centering.
w-1placeholders can shift title centering when one side control is hidden.Also applies to: 95-95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/AppHeader/AppHeader.tsx` at line 67, The title centering breaks because the small spacers (<View className="w-1" />) shrink when side controls hide; in AppHeader.tsx replace those variable tiny spacers with stable equal-width spacer slots (e.g., a fixed width/mindWidth view used on both sides of the title) so the left and right spacer elements that flank the title always occupy the same width regardless of control visibility; update the spacer instances currently at the same places (the View with className "w-1" used around the title) to use a fixed-width style/class so centering remains stable.apps/mobile/components/PendingActionPill/PendingActionPill.tsx (1)
87-105:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUnmount condition relies on shared-value reads that won’t re-render React.
if (!visible && opacity.value === 0)can stay mounted after fade-out because shared-value updates are not React state updates.In react-native-reanimated v4, do SharedValue updates trigger React component re-renders, and what is the recommended pattern for unmount-after-exit animations?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx` around lines 87 - 105, The unmount check reads the Reanimated SharedValue (opacity.value) which does not trigger React re-renders, so the component may not unmount after the fade-out; change to set a React state flag when the animation finishes: start the animations in the existing useEffect and either (A) set a setTimeout matching the 200ms duration to call setIsMounted(false) when visible becomes false, or (B) use useAnimatedReaction on the shared value (opacity) and call runOnJS to set a React state like setShouldRender(false) once opacity.value === 0 and visible is false; then use that React state (e.g., shouldRender) in the render check instead of reading opacity.value directly.apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx (1)
81-87:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign
accessibilityState.disabledwith actual disabled behavior.The control is non-interactive when
isLoadingis true, butaccessibilityState.disabledcurrently ignores that path.Proposed fix
export function SlashCommandOption({ name, description, source = "builtin", icon, isHighlighted, isLoading, className, disabled, ...props }: SlashCommandOptionProps) { + const isDisabled = Boolean(disabled || isLoading); return ( <Pressable accessibilityRole="button" accessibilityLabel={`${name} — ${description}`} accessibilityState={{ selected: isHighlighted ?? false, - disabled: disabled ?? false, + disabled: isDisabled, busy: isLoading ?? false, }} - disabled={disabled || isLoading} + disabled={isDisabled} className={cn( slashCommandOptionVariants({ isHighlighted, isLoading }), disabled && "opacity-40", className, )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx` around lines 81 - 87, Update the accessibilityState.disabled value so it reflects the actual non-interactive condition by including isLoading; specifically, in the SlashCommandOption component change the accessibilityState entry for disabled to use the same combined expression as the disabled prop (disabled || isLoading) so the accessibility state matches the rendered behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile/components/ComposerRow/ComposerRow.tsx`:
- Around line 125-131: The Stop/Send IconButton controls are interactive even
when their handlers are undefined; update the ComposerRow rendering logic (where
IconButton is used for Stop with prop onStop and for Send with prop onSend) to
guard against missing handlers by making the buttons non-interactive—either pass
a disabled flag (or set variant/shape to a non-interactive state) and omit
onPress when onStop/onSend is falsy, or render a non-button placeholder; ensure
you change both the Stop IconButton block (uses onStop) and the Send IconButton
block (uses onSend) so they are only clickable when the corresponding handler
exists.
---
Duplicate comments:
In `@apps/mobile/components/AppHeader/AppHeader.tsx`:
- Around line 58-65: The header currently renders interactive IconButton
components for back/actions even when their handlers are undefined (see
showBack/IconButton with ArrowLeft/backAccessibilityLabel and the actions block
using onActions), resulting in tappable no-ops; change the rendering so that
when onBack or onActions is falsy you either render a non-interactive element
(e.g., a static icon container) or set the IconButton to a
disabled/non-pressable state and remove the onPress prop, and ensure
accessibility reflects disabled state (so replace the IconButton's onPress when
onBack/onActions is absent and apply a disabled flag or render a plain View for
both the back (showBack) and actions sections).
- Line 67: The title centering breaks because the small spacers (<View
className="w-1" />) shrink when side controls hide; in AppHeader.tsx replace
those variable tiny spacers with stable equal-width spacer slots (e.g., a fixed
width/mindWidth view used on both sides of the title) so the left and right
spacer elements that flank the title always occupy the same width regardless of
control visibility; update the spacer instances currently at the same places
(the View with className "w-1" used around the title) to use a fixed-width
style/class so centering remains stable.
In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx`:
- Around line 87-105: The unmount check reads the Reanimated SharedValue
(opacity.value) which does not trigger React re-renders, so the component may
not unmount after the fade-out; change to set a React state flag when the
animation finishes: start the animations in the existing useEffect and either
(A) set a setTimeout matching the 200ms duration to call setIsMounted(false)
when visible becomes false, or (B) use useAnimatedReaction on the shared value
(opacity) and call runOnJS to set a React state like setShouldRender(false) once
opacity.value === 0 and visible is false; then use that React state (e.g.,
shouldRender) in the render check instead of reading opacity.value directly.
In `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx`:
- Around line 81-87: Update the accessibilityState.disabled value so it reflects
the actual non-interactive condition by including isLoading; specifically, in
the SlashCommandOption component change the accessibilityState entry for
disabled to use the same combined expression as the disabled prop (disabled ||
isLoading) so the accessibility state matches the rendered behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5ca1fe4d-309c-431e-82d2-7b665b84b23b
📒 Files selected for processing (59)
apps/mobile/components/AppHeader/AppHeader.stories.tsxapps/mobile/components/AppHeader/AppHeader.tsxapps/mobile/components/AppHeader/index.tsapps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsxapps/mobile/components/ApprovalFooter/ApprovalFooter.tsxapps/mobile/components/ApprovalFooter/index.tsapps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsxapps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsxapps/mobile/components/AssistantMessageHead/index.tsapps/mobile/components/Banner/Banner.stories.tsxapps/mobile/components/Banner/Banner.tsxapps/mobile/components/Banner/index.tsapps/mobile/components/CodeBlock/CodeBlock.stories.tsxapps/mobile/components/CodeBlock/CodeBlock.tsxapps/mobile/components/CodeBlock/index.tsapps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsxapps/mobile/components/CollapsedBlock/CollapsedBlock.tsxapps/mobile/components/CollapsedBlock/index.tsapps/mobile/components/ComposerRow/ComposerRow.stories.tsxapps/mobile/components/ComposerRow/ComposerRow.tsxapps/mobile/components/ComposerRow/index.tsapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsxapps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsxapps/mobile/components/ComposerSettingsButton/index.tsapps/mobile/components/ModalHeader/ModalHeader.stories.tsxapps/mobile/components/ModalHeader/ModalHeader.tsxapps/mobile/components/ModalHeader/index.tsapps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsxapps/mobile/components/ModelPickerOption/ModelPickerOption.tsxapps/mobile/components/ModelPickerOption/index.tsapps/mobile/components/PendingActionPill/PendingActionPill.stories.tsxapps/mobile/components/PendingActionPill/PendingActionPill.tsxapps/mobile/components/PendingActionPill/index.tsapps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsxapps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsxapps/mobile/components/PendingApprovalCard/index.tsapps/mobile/components/PickerTrigger/PickerTrigger.stories.tsxapps/mobile/components/PickerTrigger/PickerTrigger.tsxapps/mobile/components/PickerTrigger/index.tsapps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsxapps/mobile/components/ScrollBackButton/ScrollBackButton.tsxapps/mobile/components/ScrollBackButton/index.tsapps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsxapps/mobile/components/SlashCommandOption/SlashCommandOption.tsxapps/mobile/components/SlashCommandOption/index.tsapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsxapps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsxapps/mobile/components/SuggestedAnswerPill/index.tsapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsxapps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsxapps/mobile/components/ThinkingLevelOption/index.tsapps/mobile/components/ToastBase/ToastBase.stories.tsxapps/mobile/components/ToastBase/ToastBase.tsxapps/mobile/components/ToolCallCard/ToolCallCard.stories.tsxapps/mobile/components/ToolCallCard/ToolCallCard.tsxapps/mobile/components/ToolCallCard/index.tsapps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsxapps/mobile/components/UserMessageBubble/UserMessageBubble.tsxapps/mobile/components/UserMessageBubble/index.ts
✅ Files skipped from review due to trivial changes (15)
- apps/mobile/components/SlashCommandOption/index.ts
- apps/mobile/components/ComposerSettingsButton/index.ts
- apps/mobile/components/ModalHeader/index.ts
- apps/mobile/components/CodeBlock/index.ts
- apps/mobile/components/Banner/index.ts
- apps/mobile/components/AppHeader/index.ts
- apps/mobile/components/ModelPickerOption/index.ts
- apps/mobile/components/ToolCallCard/index.ts
- apps/mobile/components/ScrollBackButton/index.ts
- apps/mobile/components/ApprovalFooter/index.ts
- apps/mobile/components/ThinkingLevelOption/index.ts
- apps/mobile/components/PickerTrigger/index.ts
- apps/mobile/components/CollapsedBlock/index.ts
- apps/mobile/components/PendingActionPill/index.ts
- apps/mobile/components/AssistantMessageHead/index.ts
| <IconButton | ||
| icon={Square} | ||
| accessibilityLabel="Stop streaming" | ||
| variant="destructive" | ||
| shape="pill" | ||
| onPress={onStop} | ||
| /> |
There was a problem hiding this comment.
Prevent dead-tap Send/Stop actions when handlers are missing.
Stop and Send are rendered interactive even if onStop/onSend is undefined, which creates no-op controls.
Suggested fix
) : variant === "streaming" ? (
<IconButton
icon={Square}
accessibilityLabel="Stop streaming"
variant="destructive"
shape="pill"
onPress={onStop}
+ disabled={!onStop}
/>
) : (
<IconButton
icon={Send}
accessibilityLabel="Send message"
variant="primary"
shape="pill"
onPress={onSend}
- disabled={variant === "idle"}
+ disabled={variant === "idle" || !onSend}
/>
)}Also applies to: 133-140
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/components/ComposerRow/ComposerRow.tsx` around lines 125 - 131,
The Stop/Send IconButton controls are interactive even when their handlers are
undefined; update the ComposerRow rendering logic (where IconButton is used for
Stop with prop onStop and for Send with prop onSend) to guard against missing
handlers by making the buttons non-interactive—either pass a disabled flag (or
set variant/shape to a non-interactive state) and omit onPress when
onStop/onSend is falsy, or render a non-button placeholder; ensure you change
both the Stop IconButton block (uses onStop) and the Send IconButton block (uses
onSend) so they are only clickable when the corresponding handler exists.
Summary
Wave 2 of the chat-mobile UI build: 19 molecules composed from the wave-1 atoms (PR #4870) and vendor primitives. Each lives under `apps/mobile/components/{Name}/` with co-located `.tsx`, `.stories.tsx` (Storybook `Molecules/` prefix + full argTypes controls), and `index.ts`.
Composer cluster (UC-COMP):
Chrome cluster:
Pause cluster (UC-PAUSE):
Render cluster (UC-RENDER):
Flips `molecules` gate to `passed` for both mobile platforms. Also includes ComposerRow refinements (single-tone bg, slash icon, Claude iOS layout).
Stack position — 4 of 5
Depends on: PR #4870 (atoms) → PR #4875 (ported) → PR #4874 (tooling)
This branch contains PRs 1–3's commits + this PR's 3 commits. After PRs 1–3 merge, this will be rebased onto fresh `main` and the diff will shrink to molecules-only.
Test plan
Summary by CodeRabbit
Release Notes
New Features
Chores