Skip to content

feat(mobile): wave-3 chat-view organisms — 10 organisms (Sprint 1 / PR 5 of 7)#4872

Open
justincrich wants to merge 32 commits into
superset-sh:mainfrom
justincrich:chat-mobile-sprint-1-organisms
Open

feat(mobile): wave-3 chat-view organisms — 10 organisms (Sprint 1 / PR 5 of 7)#4872
justincrich wants to merge 32 commits into
superset-sh:mainfrom
justincrich:chat-mobile-sprint-1-organisms

Conversation

@justincrich
Copy link
Copy Markdown
Contributor

@justincrich justincrich commented May 23, 2026

Summary

Wave 3 of the chat-mobile UI build: 10 organisms composing wave-1 atoms + wave-2 molecules + vendor primitives into the named composite surfaces that views will render. Adds `@gorhom/bottom-sheet@5.2.12` as a new vendor dep.

Composites (apps/mobile/components/{Name}/):

  • ChatHeader — AppHeader + safe-area + status row + banner slot
  • ChatThread — FlatList with discriminated-union item type (user · assistant-head · assistant-body · tool-call · collapsed-block)
  • Composer — KeyboardAvoidingView + ComposerRow + suppression states (idle/typing/streaming/sending/disabled/hidden)
  • SlashCommandPopover — SlashCommandOption rows grouped by source
  • PickerPopover — generic RadioGroup picker (Model + Thinking + Permission)
  • PauseApprovalOverlay — PendingApprovalCard + sticky ApprovalFooter
  • BottomSheet — themed wrapper around @gorhom/bottom-sheet BottomSheetModal (imperative ref API)
  • PlanReviewScreen — full-screen ModalHeader + scrollable body + Reject/Approve
  • ConfirmationDialog — controlled AlertDialog wrapper
  • LoadingSkeleton — alternating-width Skeleton rows

Stories under `Organisms/` with full argTypes controls + JSX/component refs in per-story `render` (not `args` — Storybook 9 mapping). Flips `organisms` gate to `passed` for both mobile platforms.

Stack position — 5 of 5

Depends on: PR #4871 (molecules) → PR #4870 (atoms) → PR #4875 (ported) → PR #4874 (tooling)

# Branch Status
PR 1 `chat-mobile-sprint-1-tooling` #4874
PR 2 `chat-mobile-sprint-1-ported` #4875
PR 3 `chat-mobile-sprint-1-atoms` #4870
PR 4 `chat-mobile-sprint-1-molecules` #4871
PR 5 (this) `chat-mobile-sprint-1-organisms` 10 organisms

This branch contains PRs 1–4's commits + this PR's 3 commits. After PRs 1–4 merge, this will be rebased onto fresh `main` and the diff will shrink to organisms-only.

Test plan

  • `cd apps/mobile && bun storybook`
  • Navigate `Organisms/` section — all 10 organism stories render
  • BottomSheet → tap "Open sheet" → sheet animates up; tap Close or drag down to dismiss
  • ConfirmationDialog → tap trigger → modal renders; tap Cancel / Confirm
  • ChatThread → cycle through StreamingConversation / WithPlanBlock / FailedUserMessage / Empty stories
  • Composer → toggle state (idle / typing / streaming / disabled / hidden)
  • PickerPopover → switch between ModelPicker and ThinkingLevelPicker stories
  • PauseApprovalOverlay → toggle `resolving` (none / decline / approve / always)
  • `bun run typecheck` passes (28/28)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Important

Review skipped

Too many files!

This PR contains 168 files, which is 18 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6758bc9d-8496-41c4-ae16-bc835e89257b

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc8f45 and a56e52e.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (168)
  • apps/mobile/.rnstorybook/.gitignore
  • apps/mobile/.rnstorybook/StorybookRouterProvider.tsx
  • apps/mobile/.rnstorybook/index.tsx
  • apps/mobile/.rnstorybook/main.js
  • apps/mobile/.rnstorybook/mocks/tty.js
  • apps/mobile/.rnstorybook/preview.tsx
  • apps/mobile/.rnstorybook/router/LinkingContext.ts
  • apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts
  • apps/mobile/app/_layout.tsx
  • apps/mobile/components/AppHeader/AppHeader.stories.tsx
  • apps/mobile/components/AppHeader/AppHeader.tsx
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
  • apps/mobile/components/ApprovalFooter/index.ts
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx
  • apps/mobile/components/AssistantMessageHead/index.ts
  • apps/mobile/components/Banner/Banner.stories.tsx
  • apps/mobile/components/Banner/Banner.tsx
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/BottomSheet/BottomSheet.stories.tsx
  • apps/mobile/components/BottomSheet/BottomSheet.tsx
  • apps/mobile/components/BottomSheet/index.ts
  • apps/mobile/components/ChatHeader/ChatHeader.stories.tsx
  • apps/mobile/components/ChatHeader/ChatHeader.tsx
  • apps/mobile/components/ChatHeader/index.ts
  • apps/mobile/components/ChatThread/ChatThread.stories.tsx
  • apps/mobile/components/ChatThread/ChatThread.tsx
  • apps/mobile/components/ChatThread/index.ts
  • apps/mobile/components/CodeBlock/CodeBlock.stories.tsx
  • apps/mobile/components/CodeBlock/CodeBlock.tsx
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsx
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.tsx
  • apps/mobile/components/CollapsedBlock/index.ts
  • apps/mobile/components/Composer/Composer.stories.tsx
  • apps/mobile/components/Composer/Composer.tsx
  • apps/mobile/components/Composer/index.ts
  • apps/mobile/components/ComposerRow/ComposerRow.stories.tsx
  • apps/mobile/components/ComposerRow/ComposerRow.tsx
  • apps/mobile/components/ComposerRow/index.ts
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsx
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/ConfirmationDialog/ConfirmationDialog.stories.tsx
  • apps/mobile/components/ConfirmationDialog/ConfirmationDialog.tsx
  • apps/mobile/components/ConfirmationDialog/index.ts
  • apps/mobile/components/FabBase/FabBase.stories.tsx
  • apps/mobile/components/FabBase/FabBase.tsx
  • apps/mobile/components/FabBase/index.ts
  • apps/mobile/components/IconButton/IconButton.stories.tsx
  • apps/mobile/components/IconButton/IconButton.tsx
  • apps/mobile/components/IconButton/index.ts
  • apps/mobile/components/LoadingSkeleton/LoadingSkeleton.stories.tsx
  • apps/mobile/components/LoadingSkeleton/LoadingSkeleton.tsx
  • apps/mobile/components/LoadingSkeleton/index.ts
  • apps/mobile/components/ModalHeader/ModalHeader.stories.tsx
  • apps/mobile/components/ModalHeader/ModalHeader.tsx
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsx
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/PauseApprovalOverlay/PauseApprovalOverlay.stories.tsx
  • apps/mobile/components/PauseApprovalOverlay/PauseApprovalOverlay.tsx
  • apps/mobile/components/PauseApprovalOverlay/index.ts
  • apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
  • apps/mobile/components/PendingActionPill/PendingActionPill.tsx
  • apps/mobile/components/PendingActionPill/index.ts
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsx
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx
  • apps/mobile/components/PendingApprovalCard/index.ts
  • apps/mobile/components/PickerPopover/PickerPopover.stories.tsx
  • apps/mobile/components/PickerPopover/PickerPopover.tsx
  • apps/mobile/components/PickerPopover/index.ts
  • apps/mobile/components/PickerTrigger/PickerTrigger.stories.tsx
  • apps/mobile/components/PickerTrigger/PickerTrigger.tsx
  • apps/mobile/components/PickerTrigger/index.ts
  • apps/mobile/components/Pill/Pill.stories.tsx
  • apps/mobile/components/Pill/Pill.tsx
  • apps/mobile/components/Pill/index.ts
  • apps/mobile/components/PlanReviewScreen/PlanReviewScreen.stories.tsx
  • apps/mobile/components/PlanReviewScreen/PlanReviewScreen.tsx
  • apps/mobile/components/PlanReviewScreen/index.ts
  • apps/mobile/components/ProgressDots/ProgressDots.stories.tsx
  • apps/mobile/components/ProgressDots/ProgressDots.tsx
  • apps/mobile/components/ProgressDots/index.ts
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsx
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.tsx
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/ScrollFade/ScrollFade.stories.tsx
  • apps/mobile/components/ScrollFade/ScrollFade.tsx
  • apps/mobile/components/ScrollFade/index.ts
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsx
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/SlashCommandPopover/SlashCommandPopover.stories.tsx
  • apps/mobile/components/SlashCommandPopover/SlashCommandPopover.tsx
  • apps/mobile/components/SlashCommandPopover/index.ts
  • apps/mobile/components/StatusDot/StatusDot.stories.tsx
  • apps/mobile/components/StatusDot/StatusDot.tsx
  • apps/mobile/components/StatusDot/index.ts
  • apps/mobile/components/StreamingCursor/StreamingCursor.stories.tsx
  • apps/mobile/components/StreamingCursor/StreamingCursor.tsx
  • apps/mobile/components/StreamingCursor/index.ts
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsx
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx
  • apps/mobile/components/SuggestedAnswerPill/index.ts
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsx
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsx
  • apps/mobile/components/ThinkingLevelOption/index.ts
  • apps/mobile/components/ToastBase/ToastBase.stories.tsx
  • apps/mobile/components/ToastBase/ToastBase.tsx
  • apps/mobile/components/ToastBase/index.ts
  • apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx
  • apps/mobile/components/ToolCallCard/ToolCallCard.tsx
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsx
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx
  • apps/mobile/components/ToolStatusRule/index.ts
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
  • apps/mobile/components/UserMessageBubble/index.ts
  • apps/mobile/components/ui/AUDIT.md
  • apps/mobile/components/ui/accordion.stories.tsx
  • apps/mobile/components/ui/alert-dialog.stories.tsx
  • apps/mobile/components/ui/alert.stories.tsx
  • apps/mobile/components/ui/aspect-ratio.stories.tsx
  • apps/mobile/components/ui/avatar.stories.tsx
  • apps/mobile/components/ui/badge.stories.tsx
  • apps/mobile/components/ui/button.stories.tsx
  • apps/mobile/components/ui/card.stories.tsx
  • apps/mobile/components/ui/checkbox.stories.tsx
  • apps/mobile/components/ui/collapsible.stories.tsx
  • apps/mobile/components/ui/context-menu.stories.tsx
  • apps/mobile/components/ui/dialog.stories.tsx
  • apps/mobile/components/ui/dropdown-menu.stories.tsx
  • apps/mobile/components/ui/hover-card.stories.tsx
  • apps/mobile/components/ui/icon.stories.tsx
  • apps/mobile/components/ui/input.stories.tsx
  • apps/mobile/components/ui/label.stories.tsx
  • apps/mobile/components/ui/menubar.stories.tsx
  • apps/mobile/components/ui/popover.stories.tsx
  • apps/mobile/components/ui/progress.stories.tsx
  • apps/mobile/components/ui/radio-group.stories.tsx
  • apps/mobile/components/ui/select.stories.tsx
  • apps/mobile/components/ui/separator.stories.tsx
  • apps/mobile/components/ui/skeleton.stories.tsx
  • apps/mobile/components/ui/switch.stories.tsx
  • apps/mobile/components/ui/tabs.stories.tsx
  • apps/mobile/components/ui/text.stories.tsx
  • apps/mobile/components/ui/textarea.stories.tsx
  • apps/mobile/components/ui/toggle-group.stories.tsx
  • apps/mobile/components/ui/toggle.stories.tsx
  • apps/mobile/components/ui/tooltip.stories.tsx
  • apps/mobile/metro.config.js
  • apps/mobile/package.json
  • apps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsx
  • apps/mobile/screens/AUDIT.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 23, 2026

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.

@stage-review
Copy link
Copy Markdown

stage-review Bot commented May 23, 2026

@justincrich justincrich marked this pull request as draft May 23, 2026 00:03
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 178 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/HitTargetWrapper/HitTargetWrapper.tsx">

<violation number="1" location="apps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsx:65">
P2: Props spread `{...props}` is placed after accessibility defaults (`accessibilityRole`, `accessibilityState`), allowing consumers to silently override the component's intended accessibility invariants.</violation>
</file>

<file name="apps/mobile/components/ChatHeader/ChatHeader.tsx">

<violation number="1" location="apps/mobile/components/ChatHeader/ChatHeader.tsx:66">
P2: `...props` is spread after `style`, so caller-provided `style` unintentionally overwrites the safe-area top padding object.</violation>
</file>

<file name="apps/mobile/app/_layout.tsx">

<violation number="1" location="apps/mobile/app/_layout.tsx:34">
P1: Font-loading failure path leaves app stuck indefinitely: `useFonts` error state is ignored, so if font loading fails, `fontsLoaded` never becomes true, `SplashScreen.hideAsync()` is never called, and the component keeps returning `null`.</violation>
</file>

<file name="apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx">

<violation number="1" location="apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx:80">
P2: `disabled` and `resolving` props lack precedence logic, allowing a spinner to render when `disabled` is true despite docs promising "no spinner" in disabled state.</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

Comment thread apps/mobile/app/_layout.tsx Outdated
Comment thread apps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsx Outdated
return (
<View
className={cn("bg-background", className)}
style={{ paddingTop: insets.top }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ...props is spread after style, so caller-provided style unintentionally overwrites the safe-area top padding object.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/ChatHeader/ChatHeader.tsx, line 66:

<comment>`...props` is spread after `style`, so caller-provided `style` unintentionally overwrites the safe-area top padding object.</comment>

<file context>
@@ -0,0 +1,92 @@
+	return (
+		<View
+			className={cn("bg-background", className)}
+			style={{ paddingTop: insets.top }}
+			{...props}
+		>
</file context>

onPress={onDecline}
accessibilityLabel="Decline tool action"
>
{resolving === "decline" ? (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: disabled and resolving props lack precedence logic, allowing a spinner to render when disabled is true despite docs promising "no spinner" in disabled state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx, line 80:

<comment>`disabled` and `resolving` props lack precedence logic, allowing a spinner to render when `disabled` is true despite docs promising "no spinner" in disabled state.</comment>

<file context>
@@ -0,0 +1,119 @@
+						onPress={onDecline}
+						accessibilityLabel="Decline tool action"
+					>
+						{resolving === "decline" ? (
+							<ActivityIndicator size="small" className="text-white" />
+						) : (
</file context>

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`.
@justincrich justincrich force-pushed the chat-mobile-sprint-1-organisms branch from f569fe5 to 34be963 Compare May 23, 2026 00:16
@justincrich justincrich changed the title feat(mobile): wave-3 chat-view organisms — 10 organisms (Sprint 1 / PR 4 of 4) feat(mobile): wave-3 chat-view organisms — 10 organisms (Sprint 1 / PR 5 of 5) May 23, 2026
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).
@justincrich justincrich changed the title feat(mobile): wave-3 chat-view organisms — 10 organisms (Sprint 1 / PR 5 of 5) feat(mobile): wave-3 chat-view organisms — 10 organisms (Sprint 1 / PR 5 of 7) May 24, 2026
@justincrich justincrich force-pushed the chat-mobile-sprint-1-organisms branch 3 times, most recently from 001df3a to 1ecc02b Compare May 24, 2026 17:17
@justincrich justincrich marked this pull request as ready for review May 24, 2026 21:49
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 213 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/ChatHeader/ChatHeader.tsx">

<violation number="1" location="apps/mobile/components/ChatHeader/ChatHeader.tsx:66">
P2: `...props` is spread after `style`, so caller-provided `style` unintentionally overwrites the safe-area top padding object.</violation>
</file>

<file name="apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx">

<violation number="1" location="apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx:80">
P2: `disabled` and `resolving` props lack precedence logic, allowing a spinner to render when `disabled` is true despite docs promising "no spinner" in disabled state.</violation>
</file>

<file name="apps/mobile/components/AppHeader/AppHeader.tsx">

<violation number="1" location="apps/mobile/components/AppHeader/AppHeader.tsx:67">
P2: Header placeholder width (w-1 ≈ 4px) does not match the IconButton touch target width (~44px), breaking three-region symmetry and shifting the centered title when one side button is hidden.</violation>
</file>

<file name="apps/mobile/components/CodeBlock/CodeBlock.tsx">

<violation number="1" location="apps/mobile/components/CodeBlock/CodeBlock.tsx:45">
P2: Copy-feedback timer is unmanaged, leading to racey UI state on repeated taps and possible setState-after-unmount behavior.</violation>
</file>

<file name="apps/mobile/components/AppliedFilterTag/AppliedFilterTag.tsx">

<violation number="1" location="apps/mobile/components/AppliedFilterTag/AppliedFilterTag.tsx:75">
P2: Dismiss button is always shown even when `onDismiss` is undefined, creating a misleading interactive affordance and accessibility contract mismatch.</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

onPress={onBack}
/>
) : (
<View className="w-1" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Header placeholder width (w-1 ≈ 4px) does not match the IconButton touch target width (~44px), breaking three-region symmetry and shifting the centered title when one side button is hidden.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/AppHeader/AppHeader.tsx, line 67:

<comment>Header placeholder width (w-1 ≈ 4px) does not match the IconButton touch target width (~44px), breaking three-region symmetry and shifting the centered title when one side button is hidden.</comment>

<file context>
@@ -0,0 +1,99 @@
+					onPress={onBack}
+				/>
+			) : (
+				<View className="w-1" />
+			)}
+
</file context>

const handleCopy = () => {
onCopy?.(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Copy-feedback timer is unmanaged, leading to racey UI state on repeated taps and possible setState-after-unmount behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/CodeBlock/CodeBlock.tsx, line 45:

<comment>Copy-feedback timer is unmanaged, leading to racey UI state on repeated taps and possible setState-after-unmount behavior.</comment>

<file context>
@@ -0,0 +1,88 @@
+	const handleCopy = () => {
+		onCopy?.(code);
+		setCopied(true);
+		setTimeout(() => setCopied(false), 1500);
+	};
+
</file context>

/>
<Text className="text-foreground text-sm">{label}</Text>
</Pressable>
<IconButton
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Dismiss button is always shown even when onDismiss is undefined, creating a misleading interactive affordance and accessibility contract mismatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/AppliedFilterTag/AppliedFilterTag.tsx, line 75:

<comment>Dismiss button is always shown even when `onDismiss` is undefined, creating a misleading interactive affordance and accessibility contract mismatch.</comment>

<file context>
@@ -0,0 +1,87 @@
+				/>
+				<Text className="text-foreground text-sm">{label}</Text>
+			</Pressable>
+			<IconButton
+				icon={X}
+				accessibilityLabel={
</file context>

- 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.
@justincrich justincrich force-pushed the chat-mobile-sprint-1-organisms branch from 1ecc02b to 36ba413 Compare May 26, 2026 18:00
- 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).
@justincrich justincrich force-pushed the chat-mobile-sprint-1-organisms branch from 36ba413 to 4582721 Compare May 26, 2026 19:08
…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.
@justincrich justincrich force-pushed the chat-mobile-sprint-1-organisms branch from 4582721 to 2e9ecec Compare May 26, 2026 19:09
…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.
…gate

Inserts a new organisms tier between molecules and views per
designs/AUDIT.md §ORGANISM AUDIT. The 10 organisms cluster existing
molecules + vendor primitives into the named composites that views
will compose directly.

Organisms (all under apps/mobile/components/{Name}/):
  - ChatHeader            — AppHeader + safe-area + status row + banner slot
  - ChatThread            — FlatList with typed item union (user/assistant/tool/collapsed)
  - Composer              — KeyboardAvoidingView + ComposerRow + suppression states
  - SlashCommandPopover   — SlashCommandOption rows grouped by source
  - PickerPopover         — generic RadioGroup picker (Model + Thinking + Permission)
  - PauseApprovalOverlay  — PendingApprovalCard + ApprovalFooter (composer hidden)
  - BottomSheet           — themed wrapper around @gorhom/bottom-sheet BottomSheetModal
  - PlanReviewScreen      — full-screen ModalHeader + plan body + Reject/Approve
  - ConfirmationDialog    — controlled AlertDialog wrapper (Cancel/Destructive)
  - LoadingSkeleton       — alternating-width Skeleton rows for UC-SESS-02 §A

Stories all titled Organisms/{Name} with full argTypes controls and
representative fixtures (single conversation, plan block, queued
approvals, multiple sheet heights, model + thinking picker variants).

Vendor: adds @gorhom/bottom-sheet@5.2.12 (compatible with reanimated
4.3.1 + gesture-handler 2.31.2). Required by the chat-view ask_user
sheet, session overflow sheet, project / filter / new-chat pickers.

Manifest: bumps version 5.1.0 → 5.2.0, adds organisms entry to both
mobile-ios + mobile-android build_plan (10 created, vendor_added
list, source: designs/AUDIT.md §ORGANISM AUDIT), flips organisms
gate to passed, phase advances to "organisms".

Also includes biome auto-format pass on designs/* HTML/CSS/JSON
files (tab/whitespace normalization picked up by `bun run lint:fix`
during the wave 3 verification step — pure formatting, no semantic
changes).

Typecheck + lint clean for the 10 new organism folders. Wave 4
(screens / compose phase) remains deferred behind explicit user
confirmation per the manifest scope_note.
Storybook 9 RN warns when args contain JSX elements, React components,
or other non-serializable values — Storybook can't represent them in
the controls UI and recommends moving such props into `render` (or
using `mapping` for enum-style mapping).

Four organism stories had this pattern; fixed each by promoting the
JSX / component refs out of `args` into per-story `render` functions:

  - ChatHeader.stories.tsx — `banner: <Banner …/>` JSX moved into the
    OfflineWithBanner story's render(). `banner: { control: false }`
    added; status mapping aligned with ApprovalFooter pattern.
  - ChatThread.stories.tsx — message item arrays (which contain
    `body: <Text>` JSX) moved out of args into per-story renders.
    `items: { control: false, table: { disable: true } }`.
  - PauseApprovalOverlay.stories.tsx — `header: <Text>` JSX promoted
    into a const referenced by each render; resolving mapping aligned
    with ApprovalFooter pattern ("(none)" sentinel, all enum values
    explicitly mapped).
  - SlashCommandPopover.stories.tsx — `commands` arrays contain
    LucideIcon component references; moved into per-story renders.

No behavioral changes — same fixtures, same stories, just placed
where Storybook expects (render) instead of where it warns (args).

Note on the separate `UnhandledLinkingContext` error: not from this
work. None of the Wave 3 organisms or their molecule deps import
expo-router or call useNavigation/useTheme/useRouter. The known
trigger documented in apps/mobile/.rnstorybook/main.js is screen
stories transitively importing lib/theme.ts → expo-router; those
are already excluded.
…al pattern)

The previous declarative `open` / `onClose` wrapper hid the imperative
ref behind a useEffect that called `ref.current?.present()`. In
Storybook RN this pattern was unreliable — the lazy-mounted
BottomSheetModal's ref isn't connected on first render, so the
useEffect fired before the modal could respond, and tapping "Open
sheet" appeared to do nothing.

Switching to gorhom's canonical pattern:

  const ref = useRef<BottomSheetRef>(null);
  <BottomSheet ref={ref}>…</BottomSheet>
  <Button onPress={() => ref.current?.present()} />

Wrapper changes:
  - `forwardRef<BottomSheetRef, BottomSheetProps>` exposes gorhom's
    `BottomSheetModal` methods (present, dismiss, snapToIndex, etc.)
    directly to consumers
  - Drops `open` / `onClose` props + the useEffect indirection
  - `BottomSheetRef = BottomSheetModal<any>` typed to match gorhom's
    public ref signature

Story changes:
  - Imperative trigger: button onPress calls `sheetRef.current?.present()`
  - Added inline Close button inside the sheet that calls `.dismiss()`
  - Provider + GestureHandlerRootView remain at the harness root

The new API matches gorhom docs and the production chat-mobile sheet
callers (ask_user, overflow, project picker, filter, new chat) will
use the same imperative pattern when wired in Wave 4.
…6 files)

Cascades the theme revert from superset-sh#4874 through atom + molecule components:
- state-live/warning/danger/success/neutral → green-600/amber-600/destructive/muted-foreground
- streaming-cursor → foreground
- tool-rule → muted-foreground / border

Per Token Migration Table in plans/pull-all-commented-feedback-polymorphic-yao.md
@justincrich justincrich force-pushed the chat-mobile-sprint-1-organisms branch from 2e9ecec to a56e52e Compare May 26, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant