Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
deb2c2f
chore(mobile): scaffold pixel-perfect Storybook sandbox
justincrich May 22, 2026
a6b586e
chore: regenerate bun.lock after scaffold install
justincrich May 22, 2026
e0b818a
feat(mobile): scaffold ember theme — Path A token rewrite (Sprint 01 …
justincrich May 22, 2026
bbf7ab1
fix(mobile): pass AsyncStorage adapter to Storybook getStorybookUI
justincrich May 22, 2026
59eeb8a
fix(mobile): stub Node `tty` module in metro for Storybook 9.x
justincrich May 22, 2026
d91b0cd
fix(mobile): exclude screens/**/*.stories from Storybook RN main.js
justincrich May 22, 2026
5dcf47e
fix(mobile): wrap Storybook stories in expo-router NavigationContainer
justincrich May 22, 2026
2c39d89
fix(mobile): re-exclude screens stories — nav-wrapper insufficient at…
justincrich May 22, 2026
cd9786e
REMED-001: align mobile dark theme tokens
justincrich May 24, 2026
a107400
REMED-002: align mobile light theme tokens
justincrich May 24, 2026
93545ba
REMED-004: add mobile sidebar tertiary highlight tokens
justincrich May 24, 2026
272ae39
REMED-004: remove token verification spacer
justincrich May 24, 2026
f92c0c2
REMED-005: document primary semantic split (tooling half — global.css…
justincrich May 24, 2026
2762ff6
REMED-009: storybook router context (follow-up)
justincrich May 24, 2026
52add93
fix(mobile): address PR #4874 review feedback
justincrich May 26, 2026
9aee0bd
fix(mobile): revert theme to base shadcn/ui zinc palette from main
justincrich May 26, 2026
4a82191
fix(mobile): storybook nested NavigationContainer + empty index
justincrich May 26, 2026
cb71817
chore(mobile): ingest all 28 vendor primitives into Storybook (theme …
justincrich May 22, 2026
5931639
chore(mobile): migrate 8 first-party app components to ember theme (a…
justincrich May 22, 2026
309c706
chore(mobile): remove Welcome smoke story (real stories arrive in thi…
justincrich May 26, 2026
b915485
feat(mobile): add 5 chat-domain atom foundations (pre-spec)
justincrich May 22, 2026
d7a54e7
feat(mobile): wave-1 chat-view atoms — refine 5, create 5
justincrich May 22, 2026
7cc47f0
fix(mobile): ScrollFade — inline surface colors, drop lib/theme import
justincrich May 22, 2026
9929588
fix(mobile): unblock Storybook RN — drop nav import + move React elem…
justincrich May 22, 2026
36bbbb6
refactor(mobile): remove HitTargetWrapper — IconButton covers the sam…
justincrich May 26, 2026
01e524f
feat(mobile): wave-2 chat-view molecules — 19 built, flips molecules …
justincrich May 22, 2026
a04ca1a
refactor(mobile): ComposerRow → Claude iOS layout (single container)
justincrich May 22, 2026
a98b606
refactor(mobile): ComposerRow polish — single-tone bg, slash icon, ma…
justincrich May 22, 2026
c3f1728
feat(mobile): wave-3 chat-view organisms — 10 built, flips organisms …
justincrich May 22, 2026
7ed2308
fix(mobile): pull non-serializable args out of Wave 3 organism stories
justincrich May 22, 2026
6bfdca2
fix(mobile): BottomSheet wrapper → imperative ref API (gorhom canonic…
justincrich May 22, 2026
a56e52e
fix(mobile): migrate custom theme tokens to base shadcn/ui palette (1…
justincrich May 26, 2026
266c2ef
fix(mobile): pull non-serializable args out of Wave 3 organism stories
justincrich May 22, 2026
2ecad5a
REMED-006: disable PlanReviewScreen Reject button until feedback is n…
justincrich May 24, 2026
c194570
REMED-009: BottomSheet useColorScheme for light/dark gorhom theming
justincrich May 24, 2026
6f8b5b9
feat(mobile): 4 sessions-list organisms (Wave 5 — organisms portion)
justincrich May 24, 2026
d096c49
feat(mobile): 10 sessions-list view stories (Wave 5 — views portion)
justincrich May 24, 2026
e33a522
feat(mobile): 7 sessions-list molecules (Wave 5 — molecules portion)
justincrich May 24, 2026
7610f19
fix(mobile): EmptyState story polish (Wave 5 sessions-list)
justincrich May 24, 2026
4d873b8
fix(mobile): migrate text-state-warning-fg in ChatHeader stories to a…
justincrich May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/mobile/.rnstorybook/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
storybook.requires.ts
57 changes: 57 additions & 0 deletions apps/mobile/.rnstorybook/StorybookRouterProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
PreviewRouteContext,
type PreviewRouteContextType,
} from "expo-router/build/link/preview/PreviewRouteContext";
import {
LinkingContext,
NavigationContainer,
NavigationContainerRefContext,
ThemeProvider,
UNSTABLE_UnhandledLinkingContext as UnhandledLinkingContext,
} from "expo-router/react-navigation";
import { type PropsWithChildren, useContext } from "react";
import { NAV_THEME } from "@/lib/theme";

import {
storybookLinkingContext,
storybookLinkingOptions,
} from "./router/LinkingContext";
import { storybookUnhandledLinkingContext } from "./router/UnhandledLinkingContext";

const storybookRoute = {
pathname: "/storybook",
params: {},
segments: ["storybook"],
} satisfies PreviewRouteContextType;

export function StorybookRouterProvider({ children }: PropsWithChildren) {
const navigationRef = useContext(NavigationContainerRefContext);

const content = (
<ThemeProvider value={NAV_THEME.dark}>
<UnhandledLinkingContext.Provider
value={storybookUnhandledLinkingContext}
>
<LinkingContext.Provider value={storybookLinkingContext}>
<PreviewRouteContext.Provider value={storybookRoute}>
{children}
</PreviewRouteContext.Provider>
</LinkingContext.Provider>
</UnhandledLinkingContext.Provider>
</ThemeProvider>
);

if (navigationRef) {
return content;
}

return (
<NavigationContainer
fallback={null}
linking={storybookLinkingOptions}
theme={NAV_THEME.dark}
>
{content}
</NavigationContainer>
);
}
26 changes: 26 additions & 0 deletions apps/mobile/.rnstorybook/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Entry point used by app/_layout.tsx when EXPO_PUBLIC_STORYBOOK=true.
// `storybook.requires` is generated by `sb-rn-get-stories` at dev-time —
// see the "storybook" script in package.json. It is gitignored.
import AsyncStorage from "@react-native-async-storage/async-storage";
import { StorybookRouterProvider } from "./StorybookRouterProvider";
import { view } from "./storybook.requires";

// Storybook v9 react-native requires the `storage` adapter to be passed
// explicitly when `shouldPersistSelection: true` — it does not auto-detect
// AsyncStorage. Without this, attempts to read the persisted story selection
// throw `TypeError: Cannot read property 'getItem' of undefined`.
const StorybookUIRoot = view.getStorybookUI({
shouldPersistSelection: true,
storage: {
getItem: AsyncStorage.getItem,
setItem: AsyncStorage.setItem,
},
});

export default function StorybookRoot() {
return (
<StorybookRouterProvider>
<StorybookUIRoot />
</StorybookRouterProvider>
);
}
39 changes: 39 additions & 0 deletions apps/mobile/.rnstorybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** @type {import('@storybook/react-native').StorybookConfig} */
const main = {
stories: [
"./stories/**/*.stories.?(ts|tsx|js|jsx)",
"../components/**/*.stories.?(ts|tsx|js|jsx)",
// Wave 4 — chat-view view stories (pixel-perfect Phase 6 COMPOSE).
// Narrow glob points only at `screens/chat-view/` because nothing in
// that subtree imports expo-router or `useTheme`. The broader
// `../screens/**` glob remains disabled — adding it would re-trigger
// the prep-time `UnhandledLinkingContext` crash described below.
"../screens/chat-view/**/*.stories.?(ts|tsx|js|jsx)",
// Wave 5 — sessions-list view stories. Same narrow-glob constraint as
// chat-view (above): nothing in this subtree imports expo-router or
// `useTheme`, so prep-time `loadStory` is safe.
"../screens/sessions-list/**/*.stories.?(ts|tsx|js|jsx)",
// "../screens/**/*.stories.?(ts|tsx|js|jsx)",
// ^ Disabled 2026-05-22. Screen placeholder stories transitively import
// `useTheme` → `lib/theme.ts` → `expo-router/react-navigation`. Storybook 9
// RN's `loadStory` (called during `createPreparedStoryMapping`) evaluates
// each story module eagerly BEFORE decorators apply, and ends up calling
// expo-router's `useLinking` family which crashes accessing the default
// `UnhandledLinkingContext` value outside a `<NavigationContainer>`.
//
// Wrapping decorators in `<NavigationContainer>` from
// `expo-router/react-navigation` (kept in preview.tsx) does NOT help here
// because Storybook's prep-time render happens outside the decorator chain.
//
// To restore screen stories: refactor them to avoid `useTheme` / decouple
// from `lib/theme.ts` (mirror the pattern used in components/ScrollFade),
// or use `expo-router/testing-library`'s `renderRouter` helper inside a
// custom story `render` function.
],
addons: [
"@storybook/addon-ondevice-controls",
"@storybook/addon-ondevice-actions",
],
};

module.exports = main;
1 change: 1 addition & 0 deletions apps/mobile/.rnstorybook/mocks/tty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
35 changes: 35 additions & 0 deletions apps/mobile/.rnstorybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PortalHost } from "@rn-primitives/portal";
import type { Preview } from "@storybook/react-native";
import { View } from "react-native";
import { cn } from "@/lib/utils";

// NavigationContainer is provided by StorybookRouterProvider —
// do NOT add one here or SDK 56's expo-router compat check will fail.
const preview: Preview = {
decorators: [
(Story, context) => {
const isFullscreen = context.parameters?.layout === "fullscreen";
return (
<View className={cn("flex-1 bg-background", !isFullscreen && "p-4")}>
<Story />
<PortalHost />
</View>
);
},
],
parameters: {
controls: {
matchers: {
color: /(background|color|foreground)$/i,
date: /Date$/i,
},
},
moduleMock: {
mockingPairedModules: {
tty: () => require("./mocks/tty"),
},
},
},
};

export default preview;
23 changes: 23 additions & 0 deletions apps/mobile/.rnstorybook/router/LinkingContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {
LinkingOptions,
ParamListBase,
} from "expo-router/react-navigation";
import * as React from "react";

export const storybookLinkingOptions: LinkingOptions<ParamListBase> = {
enabled: false,
prefixes: [],
config: {
screens: {
Storybook: "*",
},
},
};

export const storybookLinkingContext = {
options: storybookLinkingOptions,
};

export const LinkingContext = React.createContext(storybookLinkingContext);

LinkingContext.displayName = "LinkingContext";
19 changes: 19 additions & 0 deletions apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from "react";

export type StorybookUnhandledLinkingContextValue = {
lastUnhandledLink: string | undefined;
setLastUnhandledLink: (lastUnhandledUrl: string | undefined) => void;
};

export const storybookUnhandledLinkingContext: StorybookUnhandledLinkingContextValue =
{
lastUnhandledLink: undefined,
setLastUnhandledLink: () => {},
};

export const UnhandledLinkingContext =
React.createContext<StorybookUnhandledLinkingContextValue>(
storybookUnhandledLinkingContext,
);

UnhandledLinkingContext.displayName = "UnhandledLinkingContext";
47 changes: 46 additions & 1 deletion apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,51 @@
import "react-native-get-random-values"; // MUST BE FIRST IMPORT
import "../global.css";

import {
Geist_400Regular,
Geist_500Medium,
Geist_600SemiBold,
Geist_700Bold,
useFonts,
} from "@expo-google-fonts/geist";
import {
GeistMono_400Regular,
GeistMono_500Medium,
} from "@expo-google-fonts/geist-mono";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { RootLayout } from "@/screens/RootLayout";

export default RootLayout;
SplashScreen.preventAutoHideAsync().catch(() => {
/* splash already hidden — fine to swallow */
});

const StorybookRoot =
process.env.EXPO_PUBLIC_STORYBOOK === "true"
? require("../.rnstorybook").default
: null;

export default function App() {
const [fontsLoaded, fontError] = useFonts({
Geist_400Regular,
Geist_500Medium,
Geist_600SemiBold,
Geist_700Bold,
GeistMono_400Regular,
GeistMono_500Medium,
});

useEffect(() => {
if (fontsLoaded || fontError) {
if (fontError) {
console.error("Font loading failed:", fontError);
}
SplashScreen.hideAsync().catch(() => {});
}
}, [fontsLoaded, fontError]);

if (!fontsLoaded && !fontError) return null;

const Root = StorybookRoot ?? RootLayout;
return <Root />;
}
57 changes: 57 additions & 0 deletions apps/mobile/components/AppHeader/AppHeader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Meta, StoryObj } from "@storybook/react-native";
import { Settings } from "lucide-react-native";
import { AppHeader } from "./AppHeader";

const meta: Meta<typeof AppHeader> = {
title: "Molecules/AppHeader",
component: AppHeader,
parameters: {
docs: {
description: {
component:
"Top navigation header on every chat view. Three-region flex: leading back IconButton (optional) + centered title/subtitle + trailing actions IconButton (optional). `isScrolled` adds a layered shadow for separation from scrolling content. Composes first-party IconButton + Text.",
},
},
layout: "fullscreen",
},
args: {
title: "Fix auth bug",
subtitle: "superset · main",
showBack: true,
showActions: true,
isScrolled: false,
},
argTypes: {
title: { control: "text" },
subtitle: { control: "text" },
showBack: { control: "boolean" },
showActions: { control: "boolean" },
isScrolled: { control: "boolean", description: "Adds 1px bottom shadow" },
},
};

export default meta;

type Story = StoryObj<typeof AppHeader>;

export const Default: Story = {};

export const NoSubtitle: Story = {
args: { subtitle: undefined },
};

export const NoBack: Story = {
args: { showBack: false, title: "Sessions" },
};

export const SimpleNoActions: Story = {
args: { subtitle: undefined, showActions: false },
};

export const Scrolled: Story = {
args: { isScrolled: true },
};

export const CustomActionsIcon: Story = {
args: { actionsIcon: Settings, actionsAccessibilityLabel: "Settings" },
};
Loading