diff --git a/apps/mobile/.rnstorybook/.gitignore b/apps/mobile/.rnstorybook/.gitignore
new file mode 100644
index 00000000000..575262128f0
--- /dev/null
+++ b/apps/mobile/.rnstorybook/.gitignore
@@ -0,0 +1 @@
+storybook.requires.ts
diff --git a/apps/mobile/.rnstorybook/StorybookRouterProvider.tsx b/apps/mobile/.rnstorybook/StorybookRouterProvider.tsx
new file mode 100644
index 00000000000..29d1a8bd415
--- /dev/null
+++ b/apps/mobile/.rnstorybook/StorybookRouterProvider.tsx
@@ -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 = (
+
+
+
+
+ {children}
+
+
+
+
+ );
+
+ if (navigationRef) {
+ return content;
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/apps/mobile/.rnstorybook/index.tsx b/apps/mobile/.rnstorybook/index.tsx
new file mode 100644
index 00000000000..2ec6aa32348
--- /dev/null
+++ b/apps/mobile/.rnstorybook/index.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/apps/mobile/.rnstorybook/main.js b/apps/mobile/.rnstorybook/main.js
new file mode 100644
index 00000000000..5836ab0eafa
--- /dev/null
+++ b/apps/mobile/.rnstorybook/main.js
@@ -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 ``.
+ //
+ // Wrapping decorators in `` 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;
diff --git a/apps/mobile/.rnstorybook/mocks/tty.js b/apps/mobile/.rnstorybook/mocks/tty.js
new file mode 100644
index 00000000000..f053ebf7976
--- /dev/null
+++ b/apps/mobile/.rnstorybook/mocks/tty.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/apps/mobile/.rnstorybook/preview.tsx b/apps/mobile/.rnstorybook/preview.tsx
new file mode 100644
index 00000000000..aea298c6f57
--- /dev/null
+++ b/apps/mobile/.rnstorybook/preview.tsx
@@ -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 (
+
+
+
+
+ );
+ },
+ ],
+ parameters: {
+ controls: {
+ matchers: {
+ color: /(background|color|foreground)$/i,
+ date: /Date$/i,
+ },
+ },
+ moduleMock: {
+ mockingPairedModules: {
+ tty: () => require("./mocks/tty"),
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/apps/mobile/.rnstorybook/router/LinkingContext.ts b/apps/mobile/.rnstorybook/router/LinkingContext.ts
new file mode 100644
index 00000000000..accda4e5bdc
--- /dev/null
+++ b/apps/mobile/.rnstorybook/router/LinkingContext.ts
@@ -0,0 +1,23 @@
+import type {
+ LinkingOptions,
+ ParamListBase,
+} from "expo-router/react-navigation";
+import * as React from "react";
+
+export const storybookLinkingOptions: LinkingOptions = {
+ enabled: false,
+ prefixes: [],
+ config: {
+ screens: {
+ Storybook: "*",
+ },
+ },
+};
+
+export const storybookLinkingContext = {
+ options: storybookLinkingOptions,
+};
+
+export const LinkingContext = React.createContext(storybookLinkingContext);
+
+LinkingContext.displayName = "LinkingContext";
diff --git a/apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts b/apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts
new file mode 100644
index 00000000000..d122a7052c4
--- /dev/null
+++ b/apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts
@@ -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(
+ storybookUnhandledLinkingContext,
+ );
+
+UnhandledLinkingContext.displayName = "UnhandledLinkingContext";
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
index 283df545853..aead9f5a6b0 100644
--- a/apps/mobile/app/_layout.tsx
+++ b/apps/mobile/app/_layout.tsx
@@ -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 ;
+}
diff --git a/apps/mobile/components/AppHeader/AppHeader.stories.tsx b/apps/mobile/components/AppHeader/AppHeader.stories.tsx
new file mode 100644
index 00000000000..c2554e260d0
--- /dev/null
+++ b/apps/mobile/components/AppHeader/AppHeader.stories.tsx
@@ -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 = {
+ 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;
+
+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" },
+};
diff --git a/apps/mobile/components/AppHeader/AppHeader.tsx b/apps/mobile/components/AppHeader/AppHeader.tsx
new file mode 100644
index 00000000000..ba1e4b8be83
--- /dev/null
+++ b/apps/mobile/components/AppHeader/AppHeader.tsx
@@ -0,0 +1,99 @@
+import { ArrowLeft, type LucideIcon, MoreVertical } from "lucide-react-native";
+import { View, type ViewProps } from "react-native";
+import { IconButton } from "@/components/IconButton";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+export type AppHeaderProps = ViewProps & {
+ title: string;
+ subtitle?: string;
+ /** Show the leading back button. Default true. */
+ showBack?: boolean;
+ onBack?: () => void;
+ backAccessibilityLabel?: string;
+ /** Show the trailing actions button. Default true. */
+ showActions?: boolean;
+ onActions?: () => void;
+ actionsAccessibilityLabel?: string;
+ /** Override the trailing actions icon (default MoreVertical). */
+ actionsIcon?: LucideIcon;
+ /** Adds a layered shadow for visual separation from scrolling content. */
+ isScrolled?: boolean;
+};
+
+/**
+ * Top navigation header on every chat view (UC-RENDER-01 §A, UC-SESS-04 §A).
+ *
+ * Per mol-app-header spec:
+ * - Three-region flex: leading back (optional) + centered title/subtitle + trailing actions (optional)
+ * - Subtitle (project · branch) appears below the title in --md type-meta
+ * - `isScrolled` adds a 1px bottom shadow for layered separation
+ *
+ * Composes first-party IconButton + Text.
+ */
+export function AppHeader({
+ title,
+ subtitle,
+ showBack = true,
+ onBack,
+ backAccessibilityLabel = "Back to sessions",
+ showActions = true,
+ onActions,
+ actionsAccessibilityLabel = "Session actions",
+ actionsIcon = MoreVertical,
+ isScrolled = false,
+ className,
+ ...props
+}: AppHeaderProps) {
+ return (
+
+ {showBack ? (
+
+ ) : (
+
+ )}
+
+
+
+ {title}
+
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+
+ {showActions ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/apps/mobile/components/AppHeader/index.ts b/apps/mobile/components/AppHeader/index.ts
new file mode 100644
index 00000000000..edfdab9cc64
--- /dev/null
+++ b/apps/mobile/components/AppHeader/index.ts
@@ -0,0 +1 @@
+export { AppHeader, type AppHeaderProps } from "./AppHeader";
diff --git a/apps/mobile/components/AppliedFilterTag/AppliedFilterTag.stories.tsx b/apps/mobile/components/AppliedFilterTag/AppliedFilterTag.stories.tsx
new file mode 100644
index 00000000000..ab025cd4fec
--- /dev/null
+++ b/apps/mobile/components/AppliedFilterTag/AppliedFilterTag.stories.tsx
@@ -0,0 +1,61 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { ScrollView, View } from "react-native";
+import { AppliedFilterTag } from "./AppliedFilterTag";
+
+const meta: Meta = {
+ title: "Molecules/Sessions/AppliedFilterTag",
+ component: AppliedFilterTag,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Dismissible filter chip (UC-NAV-08 §C). Workspace variant = git-branch icon + `branch · host`; status variant = colored status icon + label. Body and ✕ are separate tap targets.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ kind: "workspace",
+ label: "main · desktop",
+ onPress: () => {},
+ onDismiss: () => {},
+ },
+ argTypes: {
+ kind: {
+ control: { type: "select" },
+ options: ["workspace", "status"],
+ },
+ label: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Workspace: Story = {};
+
+export const StatusStreaming: Story = {
+ args: { kind: "status", label: "Streaming" },
+};
+
+export const HorizontalScrollRow: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
diff --git a/apps/mobile/components/AppliedFilterTag/AppliedFilterTag.tsx b/apps/mobile/components/AppliedFilterTag/AppliedFilterTag.tsx
new file mode 100644
index 00000000000..75f812efc91
--- /dev/null
+++ b/apps/mobile/components/AppliedFilterTag/AppliedFilterTag.tsx
@@ -0,0 +1,87 @@
+import { Crosshair, GitBranch, type LucideIcon, X } from "lucide-react-native";
+import { Pressable, type PressableProps, View } from "react-native";
+import { IconButton } from "@/components/IconButton";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+export type AppliedFilterTagKind = "workspace" | "status";
+
+const KIND_ICON: Record = {
+ workspace: GitBranch,
+ status: Crosshair,
+};
+
+export type AppliedFilterTagProps = Omit & {
+ kind: AppliedFilterTagKind;
+ label: string;
+ icon?: LucideIcon;
+ onDismiss?: () => void;
+ dismissAccessibilityLabel?: string;
+};
+
+/**
+ * Dismissible chip representing one applied filter (UC-NAV-08 §C).
+ *
+ * Two kinds:
+ * - workspace: git-branch icon + `branch · host` label
+ * - status: status icon (defaults to Crosshair) + status name
+ *
+ * Renders a separate dismiss button so the tap targets are unambiguous:
+ * - Tap chip body → focus/scroll-into-view (optional caller behavior)
+ * - Tap ✕ → remove this filter only
+ *
+ * Composes IconButton for the dismiss. Used inside a horizontal-scroll row
+ * that the host (ProjectChipHeader belowSearch slot) lays out.
+ */
+export function AppliedFilterTag({
+ kind,
+ label,
+ icon,
+ onDismiss,
+ onPress,
+ disabled,
+ dismissAccessibilityLabel,
+ className,
+ ...props
+}: AppliedFilterTagProps) {
+ const LeadingIcon = icon ?? KIND_ICON[kind];
+
+ return (
+
+
+
+ {label}
+
+
+
+ );
+}
diff --git a/apps/mobile/components/AppliedFilterTag/index.ts b/apps/mobile/components/AppliedFilterTag/index.ts
new file mode 100644
index 00000000000..6d798e66f26
--- /dev/null
+++ b/apps/mobile/components/AppliedFilterTag/index.ts
@@ -0,0 +1,5 @@
+export {
+ AppliedFilterTag,
+ type AppliedFilterTagKind,
+ type AppliedFilterTagProps,
+} from "./AppliedFilterTag";
diff --git a/apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx b/apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx
new file mode 100644
index 00000000000..392ebd4207b
--- /dev/null
+++ b/apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx
@@ -0,0 +1,83 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { ApprovalFooter } from "./ApprovalFooter";
+
+const meta: Meta = {
+ title: "Molecules/ApprovalFooter",
+ component: ApprovalFooter,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Sticky footer above composer during a tool-approval pause (UC-PAUSE-01 §A). Top amber ToolStatusRule + optional queue Badge + 3 Buttons (Decline / Approve / Always). Order is intentional one-handed UX: Approve in center thumb zone, Decline outer to reduce accidental taps. `resolving` dims row + spinner on indicated button. Composes ToolStatusRule + Badge + Button.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ args: {
+ queueCount: 1,
+ queueIndex: 1,
+ disabled: false,
+ },
+ argTypes: {
+ queueCount: {
+ control: { type: "number", min: 1, max: 9 },
+ description: "Total approvals queued (counter shows when > 1)",
+ },
+ queueIndex: {
+ control: { type: "number", min: 1, max: 9 },
+ description: "Current 1-indexed position in queue",
+ },
+ resolving: {
+ control: { type: "select" },
+ options: ["(none)", "decline", "approve", "always"],
+ mapping: {
+ "(none)": undefined,
+ decline: "decline",
+ approve: "approve",
+ always: "always",
+ },
+ description: "Show spinner on indicated button + dim row",
+ },
+ disabled: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Single: Story = {};
+
+export const Queued: Story = {
+ args: { queueCount: 4, queueIndex: 1 },
+};
+
+export const ResolvingApprove: Story = {
+ args: { resolving: "approve" },
+};
+
+export const ResolvingDecline: Story = {
+ args: { resolving: "decline" },
+};
+
+export const ResolvingAlways: Story = {
+ args: { resolving: "always" },
+};
+
+export const Disabled: Story = {
+ args: { disabled: true },
+};
+
+export const InContextWithRule: Story = {
+ render: () => (
+
+
+
+ {/* PendingApprovalCard would mount here in real flow */}
+
+
+
+
+ ),
+};
diff --git a/apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx b/apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
new file mode 100644
index 00000000000..f3265975d23
--- /dev/null
+++ b/apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
@@ -0,0 +1,119 @@
+import { ActivityIndicator, View, type ViewProps } from "react-native";
+import { ToolStatusRule } from "@/components/ToolStatusRule";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+export type ApprovalFooterResolvingAction = "decline" | "approve" | "always";
+
+export type ApprovalFooterProps = ViewProps & {
+ queueCount?: number;
+ queueIndex?: number;
+ onDecline?: () => void;
+ onApprove?: () => void;
+ onAlways?: () => void;
+ /** When set, dims the action row and shows a spinner on the indicated button. */
+ resolving?: ApprovalFooterResolvingAction;
+ /** Full-footer disabled (greyed out, no spinner). */
+ disabled?: boolean;
+};
+
+/**
+ * Sticky footer that docks above the composer during a tool-approval pause
+ * (UC-PAUSE-01 §A). Pairs with PendingApprovalCard in the message stream above.
+ *
+ * Per mol-approval-footer spec:
+ * - Top horizontal amber ToolStatusRule connects visually to the pending card
+ * - Optional queue counter Badge ("1 OF 4") visible when queueCount > 1
+ * - Action order: Decline · Approve · Always (intentional one-handed UX deviation
+ * from the wireframe — center is thumb-reachable for the most-common positive
+ * action, destructive is outer)
+ * - `resolving` dims the row + shows a spinner on the indicated button
+ *
+ * Composes ToolStatusRule + vendor Badge + vendor Button + ActivityIndicator.
+ */
+export function ApprovalFooter({
+ queueCount = 1,
+ queueIndex = 1,
+ onDecline,
+ onApprove,
+ onAlways,
+ resolving,
+ disabled,
+ className,
+ ...props
+}: ApprovalFooterProps) {
+ const showCounter = queueCount > 1;
+ const isResolving = resolving !== undefined;
+
+ return (
+
+
+
+ {showCounter ? (
+
+
+ {queueIndex} OF {queueCount}
+
+
+ ) : null}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/mobile/components/ApprovalFooter/index.ts b/apps/mobile/components/ApprovalFooter/index.ts
new file mode 100644
index 00000000000..62b16fca3fb
--- /dev/null
+++ b/apps/mobile/components/ApprovalFooter/index.ts
@@ -0,0 +1,5 @@
+export {
+ ApprovalFooter,
+ type ApprovalFooterProps,
+ type ApprovalFooterResolvingAction,
+} from "./ApprovalFooter";
diff --git a/apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx b/apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx
new file mode 100644
index 00000000000..3f4e696165e
--- /dev/null
+++ b/apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx
@@ -0,0 +1,72 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import {
+ AssistantMessageHead,
+ type AssistantMessageHeadVariant,
+} from "./AssistantMessageHead";
+
+const VARIANTS: AssistantMessageHeadVariant[] = [
+ "idle",
+ "streaming",
+ "thinking",
+ "paused",
+ "completed",
+];
+
+const meta: Meta = {
+ title: "Molecules/AssistantMessageHead",
+ component: AssistantMessageHead,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Header row for an assistant message (UC-RENDER-01). Avatar + ASSISTANT label + · + timestamp + optional status segment. 5 variants drive status visibility: idle (none) · streaming · thinking · paused · completed. Composes vendor Avatar + first-party StatusDot + Text.",
+ },
+ },
+ layout: "centered",
+ },
+ args: {
+ initials: "A",
+ label: "ASSISTANT",
+ timestamp: "12:43 PM",
+ variant: "idle",
+ },
+ argTypes: {
+ initials: { control: "text" },
+ label: { control: "text" },
+ timestamp: { control: "text" },
+ variant: { control: { type: "select" }, options: VARIANTS },
+ completedDuration: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Idle: Story = {};
+
+export const Streaming: Story = { args: { variant: "streaming" } };
+
+export const Thinking: Story = { args: { variant: "thinking" } };
+
+export const Paused: Story = { args: { variant: "paused" } };
+
+export const Completed: Story = {
+ args: { variant: "completed", completedDuration: "3.2s" },
+};
+
+export const AllVariants: Story = {
+ render: () => (
+
+ {VARIANTS.map((v) => (
+
+ ))}
+
+ ),
+};
diff --git a/apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx b/apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx
new file mode 100644
index 00000000000..4c83aee7807
--- /dev/null
+++ b/apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx
@@ -0,0 +1,106 @@
+import { View, type ViewProps } from "react-native";
+import { StatusDot } from "@/components/StatusDot";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+export type AssistantMessageHeadVariant =
+ | "idle"
+ | "streaming"
+ | "thinking"
+ | "paused"
+ | "completed";
+
+type VariantConfig = {
+ dotVariant?: "live" | "warning" | "success" | "neutral";
+ statusText?: string;
+ dotOpacity?: number;
+};
+
+const VARIANT: Record = {
+ idle: {},
+ streaming: { dotVariant: "live", statusText: "STREAMING" },
+ thinking: { dotVariant: "warning", statusText: "THINKING" },
+ paused: { dotVariant: "warning", statusText: "PAUSED" },
+ completed: { dotVariant: "success", statusText: "COMPLETED" },
+};
+
+export type AssistantMessageHeadProps = ViewProps & {
+ /** Initial(s) shown in the avatar fallback. Default "A". */
+ initials?: string;
+ /** Label text. Defaults to "ASSISTANT". */
+ label?: string;
+ timestamp: string;
+ variant?: AssistantMessageHeadVariant;
+ /** Extra trailing text for completed (e.g. "· 3.2s"). */
+ completedDuration?: string;
+};
+
+/**
+ * Header row for an assistant message (UC-RENDER-01).
+ *
+ * Per mol-assistant-message-head spec:
+ * - Avatar (sm accent) + ASSISTANT label + · + timestamp + optional status segment
+ * - 5 variants drive the status segment visibility + content
+ * - Non-interactive — body organism handles long-press
+ *
+ * Composes vendor Avatar + first-party StatusDot + Text.
+ */
+export function AssistantMessageHead({
+ initials = "A",
+ label = "ASSISTANT",
+ timestamp,
+ variant = "idle",
+ completedDuration,
+ className,
+ ...props
+}: AssistantMessageHeadProps) {
+ const cfg = VARIANT[variant];
+ const showStatus = variant !== "idle" && cfg.statusText;
+
+ return (
+
+
+
+
+ {initials}
+
+
+
+
+
+ {label}
+
+ ·
+
+ {timestamp}
+
+
+ {showStatus ? (
+
+ ·
+ {cfg.dotVariant ? (
+
+ ) : null}
+
+ {cfg.statusText}
+ {variant === "completed" && completedDuration
+ ? ` · ${completedDuration}`
+ : ""}
+
+
+ ) : null}
+
+ );
+}
diff --git a/apps/mobile/components/AssistantMessageHead/index.ts b/apps/mobile/components/AssistantMessageHead/index.ts
new file mode 100644
index 00000000000..5f3e0916fa5
--- /dev/null
+++ b/apps/mobile/components/AssistantMessageHead/index.ts
@@ -0,0 +1,5 @@
+export {
+ AssistantMessageHead,
+ type AssistantMessageHeadProps,
+ type AssistantMessageHeadVariant,
+} from "./AssistantMessageHead";
diff --git a/apps/mobile/components/Banner/Banner.stories.tsx b/apps/mobile/components/Banner/Banner.stories.tsx
new file mode 100644
index 00000000000..69b68a2683f
--- /dev/null
+++ b/apps/mobile/components/Banner/Banner.stories.tsx
@@ -0,0 +1,113 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Button } from "@/components/ui/button";
+import { Text } from "@/components/ui/text";
+import { Banner } from "./Banner";
+
+const meta: Meta = {
+ title: "Molecules/Banner",
+ component: Banner,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Full-width status banner above chat content. 4 variants (offline · unpaid · dispatch-failed · permission-denied) × 2 shapes (inline · stacked). Top horizontal ToolStatusRule accent in variant color. Composes ToolStatusRule + Icon + Text + IconButton.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ args: {
+ variant: "offline",
+ shape: "inline",
+ headline: "Host offline · auto-retrying in 3s",
+ },
+ argTypes: {
+ variant: {
+ control: { type: "select" },
+ options: ["offline", "unpaid", "dispatch-failed", "permission-denied"],
+ },
+ shape: {
+ control: { type: "select" },
+ options: ["inline", "stacked"],
+ },
+ headline: { control: "text" },
+ body: { control: "text", description: "Body text — stacked shape only" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Offline: Story = {};
+
+export const Unpaid: Story = {
+ args: {
+ variant: "unpaid",
+ headline: "Workspace plan upgrade required",
+ },
+ render: (args) => (
+
+ Upgrade
+
+ }
+ />
+ ),
+};
+
+export const DispatchFailed: Story = {
+ args: {
+ variant: "dispatch-failed",
+ headline: "Failed to dispatch — host unreachable",
+ },
+ render: (args) => (
+
+ Retry
+
+ }
+ />
+ ),
+};
+
+export const PermissionDeniedStacked: Story = {
+ args: {
+ variant: "permission-denied",
+ shape: "stacked",
+ headline: "Notifications disabled",
+ body: "Enable notifications in iOS Settings to receive pause approvals while the app is in the background.",
+ },
+ render: (args) => (
+
+ Open Settings →
+
+ }
+ />
+ ),
+};
+
+export const Dismissible: Story = {
+ args: {
+ headline: "Host offline · auto-retrying",
+ onDismiss: () => {},
+ },
+};
+
+export const AllInlineVariants: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
diff --git a/apps/mobile/components/Banner/Banner.tsx b/apps/mobile/components/Banner/Banner.tsx
new file mode 100644
index 00000000000..f6f56610ffa
--- /dev/null
+++ b/apps/mobile/components/Banner/Banner.tsx
@@ -0,0 +1,139 @@
+import {
+ AlertTriangle,
+ Bell,
+ type LucideIcon,
+ WifiOff,
+ X,
+} from "lucide-react-native";
+import type { ReactNode } from "react";
+import { View, type ViewProps } from "react-native";
+import { IconButton } from "@/components/IconButton";
+import { ToolStatusRule } from "@/components/ToolStatusRule";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+export type BannerVariant =
+ | "offline"
+ | "unpaid"
+ | "dispatch-failed"
+ | "permission-denied";
+
+type VariantConfig = {
+ bgClass: string;
+ textClass: string;
+ ruleVariant: "pending" | "error";
+ defaultIcon: LucideIcon;
+};
+
+const VARIANT: Record = {
+ offline: {
+ bgClass: "bg-amber-50 dark:bg-amber-950/30",
+ textClass: "text-amber-600",
+ ruleVariant: "pending",
+ defaultIcon: WifiOff,
+ },
+ unpaid: {
+ bgClass: "bg-destructive/10",
+ textClass: "text-destructive",
+ ruleVariant: "error",
+ defaultIcon: AlertTriangle,
+ },
+ "dispatch-failed": {
+ bgClass: "bg-destructive/10",
+ textClass: "text-destructive",
+ ruleVariant: "error",
+ defaultIcon: AlertTriangle,
+ },
+ "permission-denied": {
+ bgClass: "bg-amber-50 dark:bg-amber-950/30",
+ textClass: "text-amber-600",
+ ruleVariant: "pending",
+ defaultIcon: Bell,
+ },
+};
+
+export type BannerProps = ViewProps & {
+ variant?: BannerVariant;
+ shape?: "inline" | "stacked";
+ headline: string;
+ body?: string;
+ icon?: LucideIcon;
+ cta?: ReactNode;
+ onDismiss?: () => void;
+ dismissAccessibilityLabel?: string;
+};
+
+/**
+ * Full-width status banner above chat (UC-PLATF-01 + UC-PLATF-03).
+ *
+ * Per mol-banner spec:
+ * - 4 variants: offline · unpaid · dispatch-failed · permission-denied
+ * - 2 shapes: inline (icon · headline · CTA in one row) · stacked
+ * (icon+headline row, then body, then CTA below)
+ * - Top horizontal ToolStatusRule accent in variant color
+ * - Optional dismiss IconButton via onDismiss
+ *
+ * Composes ToolStatusRule + Icon + Text + IconButton.
+ */
+export function Banner({
+ variant = "offline",
+ shape = "inline",
+ headline,
+ body,
+ icon,
+ cta,
+ onDismiss,
+ dismissAccessibilityLabel = "Dismiss",
+ className,
+ ...props
+}: BannerProps) {
+ const cfg = VARIANT[variant];
+ const resolvedIcon = icon ?? cfg.defaultIcon;
+ const isStacked = shape === "stacked";
+
+ return (
+
+
+
+
+
+
+
+ {headline}
+
+
+ {!isStacked && cta ? {cta} : null}
+ {onDismiss ? (
+
+ ) : null}
+
+ {isStacked && body ? (
+
+ {body}
+
+ ) : null}
+ {isStacked && cta ? {cta} : null}
+
+
+ );
+}
diff --git a/apps/mobile/components/Banner/index.ts b/apps/mobile/components/Banner/index.ts
new file mode 100644
index 00000000000..e862ece0074
--- /dev/null
+++ b/apps/mobile/components/Banner/index.ts
@@ -0,0 +1 @@
+export { Banner, type BannerProps, type BannerVariant } from "./Banner";
diff --git a/apps/mobile/components/BottomSheet/BottomSheet.stories.tsx b/apps/mobile/components/BottomSheet/BottomSheet.stories.tsx
new file mode 100644
index 00000000000..b782525618a
--- /dev/null
+++ b/apps/mobile/components/BottomSheet/BottomSheet.stories.tsx
@@ -0,0 +1,120 @@
+import {
+ BottomSheetModalProvider,
+ BottomSheetView,
+} from "@gorhom/bottom-sheet";
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useRef } from "react";
+import { View } from "react-native";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
+import { Button } from "@/components/ui/button";
+import { Text } from "@/components/ui/text";
+import {
+ BottomSheet,
+ type BottomSheetProps,
+ type BottomSheetRef,
+} from "./BottomSheet";
+
+const SAMPLE_LINES: ReadonlyArray = [
+ "Approve · Decline · Always",
+ "Workspace: superset · main",
+ "Last touched 4 minutes ago",
+ "Tap to switch sessions",
+ "Long press for context menu",
+ "Permission mode: default",
+ "Thinking level: medium",
+ "Model: Sonnet 4.6",
+ "3 pending tool calls",
+ "Press ESC to dismiss",
+ "Drag down to close",
+ "Backdrop tap also closes",
+];
+
+type ShowcaseProps = Omit & {
+ triggerLabel: string;
+ bodyLines: number;
+};
+
+function BottomSheetShowcase({
+ triggerLabel,
+ bodyLines,
+ ...rest
+}: ShowcaseProps) {
+ const sheetRef = useRef(null);
+ const lines = SAMPLE_LINES.slice(0, Math.min(bodyLines, SAMPLE_LINES.length));
+ return (
+
+
+
+
+
+
+
+ Sheet content
+
+ {lines.map((line) => (
+
+ {line}
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Organisms/BottomSheet",
+ component: BottomSheetShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Project-themed wrapper around @gorhom/bottom-sheet BottomSheetModal. Imperative API via `useRef` — call `ref.current?.present()` to open, `.dismiss()` to close. Drag-down dismiss + tap-backdrop dismiss + themed handle/surface. Used by ask_user sheet (UC-PAUSE-02), session overflow (UC-SESS-04), new-chat picker (UC-NAV-04), project/filter pickers (UC-NAV-08).",
+ },
+ },
+ layout: "fullscreen",
+ },
+ args: {
+ triggerLabel: "Open sheet",
+ bodyLines: 4,
+ snapPoints: ["50%"],
+ enablePanDownToClose: true,
+ enableBackdropDismiss: true,
+ },
+ argTypes: {
+ triggerLabel: { control: "text" },
+ bodyLines: { control: { type: "number", min: 1, max: 12, step: 1 } },
+ enablePanDownToClose: { control: "boolean" },
+ enableBackdropDismiss: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const HalfHeight: Story = {};
+
+export const TallSheet: Story = {
+ args: {
+ snapPoints: ["80%"],
+ bodyLines: 10,
+ },
+};
+
+export const TwoSnapPoints: Story = {
+ args: {
+ snapPoints: ["30%", "75%"],
+ bodyLines: 8,
+ },
+};
diff --git a/apps/mobile/components/BottomSheet/BottomSheet.tsx b/apps/mobile/components/BottomSheet/BottomSheet.tsx
new file mode 100644
index 00000000000..f27dac6878e
--- /dev/null
+++ b/apps/mobile/components/BottomSheet/BottomSheet.tsx
@@ -0,0 +1,105 @@
+import {
+ BottomSheetBackdrop,
+ type BottomSheetBackdropProps,
+ BottomSheetHandle,
+ type BottomSheetHandleProps,
+ BottomSheetModal,
+ type BottomSheetModalProps,
+} from "@gorhom/bottom-sheet";
+import { forwardRef, useCallback, useMemo } from "react";
+import { useColorScheme } from "react-native";
+
+// biome-ignore lint/suspicious/noExplicitAny: gorhom BottomSheetModal is generic over T = any for data, matches their public ref type
+export type BottomSheetRef = BottomSheetModal;
+
+export type BottomSheetProps = Omit<
+ BottomSheetModalProps,
+ "snapPoints" | "ref"
+> & {
+ /** Snap points as percentages ("50%") or fixed pixels (300). Default: ["50%"]. */
+ snapPoints?: ReadonlyArray;
+ /** Default true — drag down to dismiss. */
+ enablePanDownToClose?: boolean;
+ /** Default true — tap backdrop to dismiss. */
+ enableBackdropDismiss?: boolean;
+};
+
+/**
+ * Project-themed wrapper around @gorhom/bottom-sheet `BottomSheetModal`.
+ *
+ * Imperative API per gorhom docs:
+ * const ref = useRef(null);
+ * …
+ *
+ }
+ />
+ ),
+};
+
+export const StackedWithCTA: Story = {
+ args: {
+ variant: "warning",
+ shape: "stacked",
+ title: "Host offline",
+ body: "The macbook-pro host hasn't responded in 45 seconds. New messages will queue until it reconnects.",
+ onDismiss: () => {},
+ },
+ render: (args) => (
+
+ Reconnect
+
+ }
+ />
+ ),
+};
+
+export const AllVariantsInline: Story = {
+ render: () => (
+
+
+
+
+
+
+
+ ),
+};
diff --git a/apps/mobile/components/ToastBase/ToastBase.tsx b/apps/mobile/components/ToastBase/ToastBase.tsx
new file mode 100644
index 00000000000..76799be0ae9
--- /dev/null
+++ b/apps/mobile/components/ToastBase/ToastBase.tsx
@@ -0,0 +1,184 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import {
+ AlertCircle,
+ AlertTriangle,
+ CheckCircle2,
+ Info,
+ type LucideIcon,
+ X,
+} from "lucide-react-native";
+import type { ReactNode } from "react";
+import { ActivityIndicator, View, type ViewProps } from "react-native";
+import { IconButton } from "@/components/IconButton";
+import { ToolStatusRule } from "@/components/ToolStatusRule";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+const toastVariants = cva(
+ "flex-row items-stretch overflow-hidden rounded-xl border border-border bg-popover shadow-lg",
+ {
+ variants: {
+ variant: {
+ info: "",
+ success: "",
+ warning: "",
+ danger: "",
+ loading: "",
+ },
+ shape: {
+ inline: "",
+ stacked: "",
+ },
+ },
+ defaultVariants: {
+ variant: "info",
+ shape: "inline",
+ },
+ },
+);
+
+export type ToastBaseVariant =
+ | "info"
+ | "success"
+ | "warning"
+ | "danger"
+ | "loading";
+
+type Mapping = {
+ ruleVariant: "running" | "done" | "pending" | "error" | "neutral";
+ defaultIcon: LucideIcon;
+ iconColor: string;
+};
+
+const variantMapping: Record = {
+ info: {
+ ruleVariant: "neutral",
+ defaultIcon: Info,
+ iconColor: "text-primary",
+ },
+ success: {
+ ruleVariant: "done",
+ defaultIcon: CheckCircle2,
+ iconColor: "text-green-600",
+ },
+ warning: {
+ ruleVariant: "pending",
+ defaultIcon: AlertTriangle,
+ iconColor: "text-amber-600",
+ },
+ danger: {
+ ruleVariant: "error",
+ defaultIcon: AlertCircle,
+ iconColor: "text-destructive",
+ },
+ loading: {
+ ruleVariant: "running",
+ defaultIcon: Info,
+ iconColor: "text-muted-foreground",
+ },
+};
+
+export type ToastBaseProps = ViewProps &
+ VariantProps & {
+ variant?: ToastBaseVariant;
+ shape?: "inline" | "stacked";
+ body: string;
+ /** Stacked shape only — bolded title above the body. */
+ title?: string;
+ /** Override the default lucide icon for this variant. */
+ icon?: LucideIcon;
+ /** Optional action button slot (e.g. Retry, Open Settings). */
+ action?: ReactNode;
+ /** When provided, renders a ✕ dismiss button via IconButton (44pt touch target). */
+ onDismiss?: () => void;
+ dismissAccessibilityLabel?: string;
+ accessibilityLabel?: string;
+ };
+
+/**
+ * Single transient notification surface. Caller manages timeout + position.
+ *
+ * Per atom · toast-base spec:
+ * - 5 variants: info (default) · success · warning · danger · loading.
+ * - 2 shapes: inline (icon · body · actions in one row) · stacked
+ * (title row → body row → action row, useful for longer messages).
+ * - Variant color is conveyed via the 3px left ToolStatusRule + matching
+ * icon tint; the surface itself stays neutral (bg-popover) for legibility.
+ * - `loading` swaps the leading icon for ActivityIndicator.
+ * - `onDismiss` renders ✕ via IconButton (variant=ghost, shape=pill) for a
+ * guaranteed 44pt touch target.
+ *
+ * Composes existing internals: ToolStatusRule + Icon + Text + IconButton.
+ * No external vendor component fits the left-rule layout cleanly.
+ */
+export function ToastBase({
+ variant = "info",
+ shape = "inline",
+ body,
+ title,
+ icon,
+ action,
+ onDismiss,
+ dismissAccessibilityLabel,
+ accessibilityLabel,
+ className,
+ ...props
+}: ToastBaseProps) {
+ const mapping = variantMapping[variant];
+ const resolvedIcon = icon ?? mapping.defaultIcon;
+ const isStacked = shape === "stacked";
+
+ return (
+
+
+
+
+ {variant === "loading" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isStacked && title ? (
+ {title}
+ ) : null}
+ {body}
+ {isStacked && action ? {action} : null}
+
+
+ {!isStacked && action ? {action} : null}
+ {onDismiss ? (
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/mobile/components/ToastBase/index.ts b/apps/mobile/components/ToastBase/index.ts
new file mode 100644
index 00000000000..96532dd8b58
--- /dev/null
+++ b/apps/mobile/components/ToastBase/index.ts
@@ -0,0 +1,5 @@
+export {
+ ToastBase,
+ type ToastBaseProps,
+ type ToastBaseVariant,
+} from "./ToastBase";
diff --git a/apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx b/apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx
new file mode 100644
index 00000000000..0446e537e00
--- /dev/null
+++ b/apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx
@@ -0,0 +1,118 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import {
+ Code,
+ FileText,
+ Globe,
+ type LucideIcon,
+ Search,
+ Terminal,
+} from "lucide-react-native";
+import { View } from "react-native";
+import { ToolCallCard, type ToolCallStatus } from "./ToolCallCard";
+
+const STATUSES: ToolCallStatus[] = [
+ "running",
+ "done",
+ "pending",
+ "error",
+ "neutral",
+];
+
+const ICON_MAP: Record = {
+ Terminal,
+ Code,
+ FileText,
+ Search,
+ Globe,
+};
+
+const meta: Meta = {
+ title: "Molecules/ToolCallCard",
+ component: ToolCallCard,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Collapsed tool-call card (UC-RENDER-04). Tappable to navigate to detail; never expands in-place. 5 status variants drive ToolStatusRule + status Pill + icon color. running shows ActivityIndicator inline. Composes ToolStatusRule + Pill + Icon + Text.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ args: {
+ name: "bash",
+ args: "$ bun test --filter billing",
+ status: "running",
+ duration: "0.3s",
+ },
+ argTypes: {
+ name: { control: "text" },
+ args: { control: "text" },
+ status: { control: { type: "select" }, options: STATUSES },
+ icon: {
+ control: { type: "select" },
+ options: Object.keys(ICON_MAP),
+ mapping: ICON_MAP,
+ },
+ duration: { control: "text", description: "Appended to DONE label" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Running: Story = {};
+
+export const Done: Story = {
+ args: { status: "done", duration: "0.3s" },
+};
+
+export const Pending: Story = {
+ args: { status: "pending" },
+};
+
+export const Failed: Story = {
+ args: { status: "error", args: "Error: ENOENT — file not found" },
+};
+
+export const Neutral: Story = {
+ args: { status: "neutral" },
+};
+
+export const AllStatuses: Story = {
+ render: () => (
+
+
+
+
+
+
+
+ ),
+};
diff --git a/apps/mobile/components/ToolCallCard/ToolCallCard.tsx b/apps/mobile/components/ToolCallCard/ToolCallCard.tsx
new file mode 100644
index 00000000000..6d0d65bea43
--- /dev/null
+++ b/apps/mobile/components/ToolCallCard/ToolCallCard.tsx
@@ -0,0 +1,149 @@
+import { ChevronRight, Cog, type LucideIcon } from "lucide-react-native";
+import {
+ ActivityIndicator,
+ Pressable,
+ type PressableProps,
+ View,
+} from "react-native";
+import { Pill } from "@/components/Pill";
+import { ToolStatusRule } from "@/components/ToolStatusRule";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+export type ToolCallStatus =
+ | "running"
+ | "done"
+ | "pending"
+ | "error"
+ | "neutral";
+
+type StatusConfig = {
+ ruleVariant: "running" | "done" | "pending" | "error" | "neutral";
+ statusPillVariant: "live" | "default" | "warning" | "danger" | "default";
+ statusLabel?: string;
+ iconColorClass: string;
+ showSpinner?: boolean;
+};
+
+const STATUS: Record = {
+ running: {
+ ruleVariant: "running",
+ statusPillVariant: "live",
+ statusLabel: "RUNNING",
+ iconColorClass: "text-green-600",
+ showSpinner: true,
+ },
+ done: {
+ ruleVariant: "done",
+ statusPillVariant: "default",
+ statusLabel: "DONE",
+ iconColorClass: "text-muted-foreground",
+ },
+ pending: {
+ ruleVariant: "pending",
+ statusPillVariant: "warning",
+ statusLabel: "AWAITING",
+ iconColorClass: "text-amber-600",
+ },
+ error: {
+ ruleVariant: "error",
+ statusPillVariant: "danger",
+ statusLabel: "FAILED",
+ iconColorClass: "text-destructive",
+ },
+ neutral: {
+ ruleVariant: "neutral",
+ statusPillVariant: "default",
+ iconColorClass: "text-muted-foreground/60",
+ },
+};
+
+export type ToolCallCardProps = PressableProps & {
+ name: string;
+ args?: string;
+ status?: ToolCallStatus;
+ icon?: LucideIcon;
+ /** e.g. "0.3s" appended to DONE label. */
+ duration?: string;
+};
+
+/**
+ * Collapsed tool-call card (UC-RENDER-04). Tappable to navigate to detail view;
+ * never expands in-place.
+ *
+ * Per mol-tool-call-card spec:
+ * - 5 status variants drive ToolStatusRule + status Pill + icon color
+ * - running shows inline ActivityIndicator instead of static spinner
+ * - tool name in monospace Pill (default variant)
+ * - args preview shown as truncated mono line below header
+ * - trailing chevron-right indicates tappability
+ *
+ * Composes ToolStatusRule + first-party Pill + Icon + Text + ActivityIndicator.
+ */
+export function ToolCallCard({
+ name,
+ args,
+ status = "running",
+ icon = Cog,
+ duration,
+ disabled,
+ className,
+ ...props
+}: ToolCallCardProps) {
+ const cfg = STATUS[status];
+ const statusText =
+ status === "done" && duration
+ ? `${cfg.statusLabel} · ${duration}`
+ : cfg.statusLabel;
+
+ return (
+
+
+
+
+
+
+
+ {statusText ? (
+
+ {cfg.showSpinner ? (
+
+ ) : null}
+
+
+ ) : null}
+
+ {args ? (
+
+
+ {args}
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/mobile/components/ToolCallCard/index.ts b/apps/mobile/components/ToolCallCard/index.ts
new file mode 100644
index 00000000000..586df1afad8
--- /dev/null
+++ b/apps/mobile/components/ToolCallCard/index.ts
@@ -0,0 +1,5 @@
+export {
+ ToolCallCard,
+ type ToolCallCardProps,
+ type ToolCallStatus,
+} from "./ToolCallCard";
diff --git a/apps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsx b/apps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsx
new file mode 100644
index 00000000000..b9cef7b651d
--- /dev/null
+++ b/apps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsx
@@ -0,0 +1,124 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Text } from "@/components/ui/text";
+import { ToolStatusRule } from "./ToolStatusRule";
+
+type Variant = "running" | "done" | "pending" | "error" | "neutral";
+
+function ToolStatusRuleShowcase({
+ variant,
+ orientation,
+}: {
+ variant: Variant;
+ orientation: "vertical" | "horizontal";
+}) {
+ if (orientation === "horizontal") {
+ return (
+
+
+
+ approval-footer
+
+ horizontal rule · variant={variant}
+
+
+
+ );
+ }
+ return (
+
+
+
+ ReadFile
+
+ src/handlers/chat.ts · 1.2KB · variant={variant}
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/ToolStatusRule",
+ component: ToolStatusRuleShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "3px colored rule indicating tool-call / approval status. Five status variants (running · done · pending · error · neutral) × two orientations (vertical default · horizontal for approval-footer). `running` and `pending` carry a soft glow via boxShadow.",
+ },
+ },
+ },
+ args: {
+ variant: "running",
+ orientation: "vertical",
+ },
+ argTypes: {
+ variant: {
+ control: { type: "select" },
+ options: ["running", "done", "pending", "error", "neutral"],
+ description:
+ "Status palette — running (mint + glow) · done (green) · pending (amber + glow) · error (red) · neutral (gray)",
+ },
+ orientation: {
+ control: { type: "select" },
+ options: ["vertical", "horizontal"],
+ description:
+ "vertical (default, left-edge of tool-call cards) or horizontal (top-edge of approval-footer)",
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Running: Story = {};
+export const Done: Story = { args: { variant: "done" } };
+export const Pending: Story = { args: { variant: "pending" } };
+export const ErrorVariant: Story = { args: { variant: "error" } };
+export const Neutral: Story = { args: { variant: "neutral" } };
+
+export const HorizontalRunning: Story = {
+ args: { orientation: "horizontal", variant: "running" },
+};
+
+export const AllVariantsVertical: Story = {
+ render: () => (
+
+ {(["running", "done", "pending", "error", "neutral"] as const).map(
+ (v) => (
+
+
+
+ {v}
+
+
+ ),
+ )}
+
+ ),
+};
+
+export const AllVariantsHorizontal: Story = {
+ render: () => (
+
+ {(["running", "done", "pending", "error", "neutral"] as const).map(
+ (v) => (
+
+
+
+ {v}
+
+
+ ),
+ )}
+
+ ),
+};
diff --git a/apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx b/apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx
new file mode 100644
index 00000000000..ba91b34fba2
--- /dev/null
+++ b/apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx
@@ -0,0 +1,94 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import { View, type ViewProps } from "react-native";
+import { cn } from "@/lib/utils";
+
+const toolStatusRuleVariants = cva("rounded-sm", {
+ variants: {
+ variant: {
+ running: "bg-green-600",
+ done: "bg-green-600",
+ pending: "bg-amber-600",
+ error: "bg-destructive",
+ neutral: "bg-muted-foreground",
+ },
+ orientation: {
+ // 3px = --domain-tool-rule-width per spec. Fixed; not configurable.
+ vertical: "w-[3px] self-stretch min-h-6",
+ horizontal: "h-[3px] w-full",
+ },
+ },
+ defaultVariants: {
+ variant: "running",
+ orientation: "vertical",
+ },
+});
+
+type ToolStatusRuleVariant = NonNullable<
+ VariantProps["variant"]
+>;
+
+// RN custom-colored shadows have no NativeWind utility — keep these inline.
+const glowStyleByVariant: Record<
+ ToolStatusRuleVariant,
+ ViewProps["style"] | undefined
+> = {
+ 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 },
+ },
+ done: undefined,
+ error: undefined,
+ neutral: undefined,
+};
+
+export type ToolStatusRuleProps = ViewProps &
+ VariantProps;
+
+/**
+ * 3px colored rule indicating tool-call / approval status. Composed by
+ * tool-call-card, plan/reasoning blocks, pending-approval-card, approval-footer.
+ *
+ * Per atom · tool-status-rule spec:
+ * - Status variants: running · done · pending · error · neutral.
+ * - Orientation: vertical (default, self-stretch + min-h-24) or horizontal
+ * (full-width × 3px tall, for approval-footer top edge).
+ * - `running` + `pending` carry a soft glow via shadowColor — kept inline
+ * because RN custom-colored shadows have no NativeWind utility equivalent.
+ *
+ * Decorative — `aria-hidden`. Status meaning MUST also be conveyed by the
+ * parent card via text + icon.
+ */
+export function ToolStatusRule({
+ variant,
+ orientation,
+ className,
+ style,
+ ...props
+}: ToolStatusRuleProps) {
+ const resolvedVariant: ToolStatusRuleVariant = variant ?? "running";
+ const resolvedOrientation = orientation ?? "vertical";
+ const glow = glowStyleByVariant[resolvedVariant];
+ return (
+
+ );
+}
diff --git a/apps/mobile/components/ToolStatusRule/index.ts b/apps/mobile/components/ToolStatusRule/index.ts
new file mode 100644
index 00000000000..efe3e7b06d7
--- /dev/null
+++ b/apps/mobile/components/ToolStatusRule/index.ts
@@ -0,0 +1,2 @@
+export type { ToolStatusRuleProps } from "./ToolStatusRule";
+export { ToolStatusRule } from "./ToolStatusRule";
diff --git a/apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx b/apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx
new file mode 100644
index 00000000000..3cbe8eee746
--- /dev/null
+++ b/apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx
@@ -0,0 +1,70 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { UserMessageBubble } from "./UserMessageBubble";
+
+const meta: Meta = {
+ title: "Molecules/UserMessageBubble",
+ component: UserMessageBubble,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "User's outgoing message — right-aligned bubble with styled surface. 3 variants — default · accent (@mentions ember) · pending (optimistic 60% opacity). failed=true swaps timestamp for 'Failed to send' + Retry Button. Long-press opens copy/share via onLongPress. Composes Pressable + Button + Text.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ args: {
+ message: "The relay tunnel is reconnecting on every Wi-Fi flap.",
+ timestamp: "12:42 PM",
+ variant: "default",
+ failed: false,
+ },
+ argTypes: {
+ message: { control: "text" },
+ timestamp: { control: "text" },
+ variant: {
+ control: { type: "select" },
+ options: ["default", "accent", "pending"],
+ },
+ failed: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Accent: Story = {
+ args: { variant: "accent", message: "@assistant please check this out" },
+};
+
+export const Pending: Story = {
+ args: { variant: "pending", timestamp: undefined },
+};
+
+export const Failed: Story = {
+ args: { failed: true, timestamp: undefined },
+};
+
+export const InThread: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+};
diff --git a/apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx b/apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
new file mode 100644
index 00000000000..876aa3aac6f
--- /dev/null
+++ b/apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
@@ -0,0 +1,88 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import { Pressable, View, type ViewProps } from "react-native";
+import { Button } from "@/components/ui/button";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+const bubbleVariants = cva(
+ "max-w-[85%] self-end rounded-2xl px-4 py-2.5 active:opacity-80",
+ {
+ variants: {
+ variant: {
+ default: "bg-card",
+ accent: "bg-primary/15",
+ pending: "bg-card opacity-60",
+ },
+ },
+ defaultVariants: { variant: "default" },
+ },
+);
+
+export type UserMessageBubbleProps = ViewProps &
+ VariantProps & {
+ message: string;
+ timestamp?: string;
+ /** Show "Failed to send" + Retry button under the bubble. */
+ failed?: boolean;
+ onLongPress?: () => void;
+ onRetry?: () => void;
+ };
+
+/**
+ * User's outgoing message in chat (UC-RENDER-01). Right-aligned bubble with
+ * styled surface; long-press triggers the copy/share context menu (host wires
+ * onLongPress to a native ActionSheet).
+ *
+ * Per mol-user-message-bubble spec:
+ * - 3 variants: default · accent (ember @mentions) · pending (optimistic, 60% opacity)
+ * - failed=true → meta swap to "Failed to send" + inline Retry button
+ * - long-press affordance via `aria-haspopup="menu"` semantics
+ *
+ * Composes Pressable + vendor Button + Text.
+ */
+export function UserMessageBubble({
+ message,
+ timestamp,
+ failed,
+ variant,
+ onLongPress,
+ onRetry,
+ className,
+ ...props
+}: UserMessageBubbleProps) {
+ return (
+
+
+ {message}
+
+
+ {failed ? (
+ <>
+ Failed to send
+
+ Retry
+
+ >
+ ) : timestamp ? (
+ {timestamp}
+ ) : null}
+
+
+ );
+}
diff --git a/apps/mobile/components/UserMessageBubble/index.ts b/apps/mobile/components/UserMessageBubble/index.ts
new file mode 100644
index 00000000000..5c53c483adf
--- /dev/null
+++ b/apps/mobile/components/UserMessageBubble/index.ts
@@ -0,0 +1,4 @@
+export {
+ UserMessageBubble,
+ type UserMessageBubbleProps,
+} from "./UserMessageBubble";
diff --git a/apps/mobile/components/WorkspacePickerRow/WorkspacePickerRow.stories.tsx b/apps/mobile/components/WorkspacePickerRow/WorkspacePickerRow.stories.tsx
new file mode 100644
index 00000000000..7058fbfbc50
--- /dev/null
+++ b/apps/mobile/components/WorkspacePickerRow/WorkspacePickerRow.stories.tsx
@@ -0,0 +1,65 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { WorkspacePickerRow } from "./WorkspacePickerRow";
+
+const meta: Meta = {
+ title: "Molecules/Sessions/WorkspacePickerRow",
+ component: WorkspacePickerRow,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Row in the NewChatSheet workspace picker (UC-NAV §D). Branch + host icon + host name on line 1; subtitle (sessions count + recency or 'no sessions yet') on line 2.",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: {
+ branch: "chat-mobile-plan",
+ hostName: "macbook",
+ hostKind: "laptop",
+ subtitle: "5 sessions · 2m ago",
+ showChevron: true,
+ onPress: () => {},
+ },
+ argTypes: {
+ branch: { control: "text" },
+ hostName: { control: "text" },
+ hostKind: {
+ control: { type: "select" },
+ options: ["laptop", "cloud"],
+ },
+ subtitle: { control: "text" },
+ showChevron: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Active: Story = {};
+
+export const CloudHost: Story = {
+ args: {
+ branch: "api-rewrite",
+ hostName: "cloud-1",
+ hostKind: "cloud",
+ subtitle: "3 sessions · 1h ago",
+ },
+};
+
+export const EmptyWorkspace: Story = {
+ args: {
+ branch: "feature-x",
+ hostName: "cloud-1",
+ hostKind: "cloud",
+ subtitle: "no sessions yet",
+ },
+};
diff --git a/apps/mobile/components/WorkspacePickerRow/WorkspacePickerRow.tsx b/apps/mobile/components/WorkspacePickerRow/WorkspacePickerRow.tsx
new file mode 100644
index 00000000000..61bf7eb1db9
--- /dev/null
+++ b/apps/mobile/components/WorkspacePickerRow/WorkspacePickerRow.tsx
@@ -0,0 +1,88 @@
+import {
+ ChevronRight,
+ Cloud,
+ GitBranch,
+ Laptop,
+ type LucideIcon,
+} from "lucide-react-native";
+import { Pressable, type PressableProps, View } from "react-native";
+import type { SessionHostKind } from "@/components/SessionRow";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+const HOST_ICON: Record = {
+ laptop: Laptop,
+ cloud: Cloud,
+};
+
+export type WorkspacePickerRowProps = Omit & {
+ branch: string;
+ hostName: string;
+ hostKind?: SessionHostKind;
+ /** Subtitle line — e.g. "5 sessions · 2m ago" or "no sessions yet". */
+ subtitle?: string;
+ /** Trailing chevron — hidden when explicitly false. */
+ showChevron?: boolean;
+};
+
+/**
+ * Row in the NewChatSheet workspace picker (UC-NAV §D). Composes:
+ * - Leading git-branch IconGlyph
+ * - Body: branch · host icon + host (line 1) + subtitle (line 2)
+ * - Trailing chevron
+ *
+ * 44pt minimum row height. Tappable; long-press is not used here.
+ */
+export function WorkspacePickerRow({
+ branch,
+ hostName,
+ hostKind = "laptop",
+ subtitle,
+ showChevron = true,
+ onPress,
+ disabled,
+ className,
+ ...props
+}: WorkspacePickerRowProps) {
+ const HostIcon = HOST_ICON[hostKind];
+
+ return (
+
+
+
+
+
+ {branch}
+
+
+ ·
+
+
+
+ {hostName}
+
+
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+ {showChevron ? (
+
+ ) : null}
+
+ );
+}
diff --git a/apps/mobile/components/WorkspacePickerRow/index.ts b/apps/mobile/components/WorkspacePickerRow/index.ts
new file mode 100644
index 00000000000..41a85aac666
--- /dev/null
+++ b/apps/mobile/components/WorkspacePickerRow/index.ts
@@ -0,0 +1,4 @@
+export {
+ WorkspacePickerRow,
+ type WorkspacePickerRowProps,
+} from "./WorkspacePickerRow";
diff --git a/apps/mobile/components/ui/AUDIT.md b/apps/mobile/components/ui/AUDIT.md
new file mode 100644
index 00000000000..2c532fe5755
--- /dev/null
+++ b/apps/mobile/components/ui/AUDIT.md
@@ -0,0 +1,99 @@
+# Vendor Primitive Token-Bypass Audit
+
+**Generated:** 2026-05-22
+**Scope:** `apps/mobile/components/ui/*.tsx` (28 files)
+**Rule reference:** `vendor_components_immutable` constraint in `apps/mobile/design/manifest.json`
+
+This audit catalogs **hardcoded color, opacity, and spacing values** in the vendor `react-native-reusables` primitives that bypass the token system in `apps/mobile/global.css`. Under the **vendor immutable** rule, these are NOT edited locally — the resolution path is an upstream PR to `react-native-reusables` (or a documented divergence accepted on our side).
+
+Listed values render correctly under the ember theme today; they just don't *participate* in the token system, so a future token change (e.g. shadow opacity tuning) won't reach them.
+
+---
+
+## Findings
+
+### 1. `shadow-black/5` and `shadow-black/N` — theme-blind shadows
+
+**Files (24+ instances):**
+- `alert-dialog.tsx` (1)
+- `button.tsx` (5 — default / destructive / outline / secondary / link variants)
+- `card.tsx` (1)
+- `checkbox.tsx` (1)
+- `context-menu.tsx` (2)
+- `dialog.tsx` (1)
+- `dropdown-menu.tsx` (2)
+- `hover-card.tsx` (1)
+- `input.tsx` (1)
+- `menubar.tsx` (3)
+- `popover.tsx` (1)
+- `radio-group.tsx` (1)
+- `select.tsx` (2)
+- `switch.tsx` (1)
+- `tabs.tsx` (1)
+- `textarea.tsx` (1)
+- `toggle-group.tsx` (1)
+- `toggle.tsx` (1)
+
+**Issue:** Tailwind's literal `black` color used at 5% opacity (`shadow-black/5`) regardless of theme. Under our warm-neutral ember dark surface (`#151110`), a 5% black shadow is nearly invisible — the shadow blends into the already-dark page. Under light theme, it works as intended.
+
+**User-visible impact:** Low. Drop shadows in dark mode are subtle to invisible — but rn-reusables design intent is *also* subtle. Net visual delta is small.
+
+**Future-proof fix path:** Replace with a token like `shadow-foreground/5` (uses theme foreground color) or introduce explicit `--color-shadow-soft` / `--color-shadow-overlay` tokens. Either way, the change happens upstream (`react-native-reusables`) — we do NOT patch locally.
+
+### 2. `bg-black/50` — backdrop overlay
+
+**Files (2 instances):**
+- `alert-dialog.tsx:32` — modal backdrop
+- `dialog.tsx:34` — modal backdrop
+
+**Issue:** Backdrop dim layer hardcoded to 50% black. Theme-blind by design — black backdrop on light theme dims the page to dark-gray; black backdrop on dark theme dims to near-pure-black (the page is already dark, so the dim is subtle).
+
+**User-visible impact:** Acceptable. A black backdrop is the conventional "modal dim" treatment across iOS/Android/web. Our dark-theme dim is *more* aggressive than a light-theme dim, which arguably matches user expectation (dark modals against dark UI need a stronger dim to feel layered).
+
+**Future-proof fix path:** Optional. If we ever want theme-aware backdrops (rare), introduce `--color-backdrop` token.
+
+### 3. `text-white` in destructive variants
+
+**Files (2 instances):**
+- `button.tsx:76` — `buttonTextVariants.destructive`
+- `badge.tsx:45` — `badgeTextVariants.destructive`
+
+**Issue:** Hardcoded white text on destructive backgrounds. The token `--color-destructive-foreground` is defined in `global.css` as:
+- Light: `hsl(0 0% 100%)` (pure white) — `text-white` is identical
+- Dark: `hsl(0 100% 90%)` (light pink) — `text-white` is slightly cooler than the warm pink token
+
+**User-visible impact:** Negligible. On dark surfaces, `text-white` vs. `hsl(0 100% 90%)` is indistinguishable to most users.
+
+**Future-proof fix path:** Replace literals with `text-destructive-foreground`. Same conclusion — upstream PR, not a local edit.
+
+---
+
+## Why we don't fix these locally
+
+The `vendor_components_immutable` constraint exists because:
+
+1. **rn-reusables CLI overwrites local edits.** If anyone runs `npx @react-native-reusables/cli@latest add button`, every local tweak to `button.tsx` is silently wiped.
+2. **Silent drift defeats the point of vendor-managed components.** The value of using upstream primitives is they stay aligned with upstream. Forking destroys that.
+3. **Hand-rolling wrappers around vendor components creates the same problem at one level of indirection.** A `` that wraps `` to inject `text-destructive-foreground` is just a slower way to fork.
+
+The right fixes for the above findings:
+
+| Finding | Upstream resolution |
+|---|---|
+| `shadow-black/5` | Open issue or PR on `react-native-reusables` proposing theme-aware shadow tokens |
+| `bg-black/50` | Acceptable as-is; only revisit if a designer flags it |
+| `text-white` in destructive | Open PR on `react-native-reusables` replacing with `text-destructive-foreground` |
+
+Until upstream PRs land, these divergences are **acknowledged and accepted** — they don't break ember rendering, they just don't participate in the token system.
+
+---
+
+## Story coverage status
+
+All 28 primitives now have stories under `Components/{Primitive}` (added 2026-05-22) — visually inspectable in Storybook on iOS Simulator + Android Emulator. Use the Storybook walkthrough as the verification surface; flag any visual regression discovered there back into this doc.
+
+---
+
+## Out-of-scope utilities
+
+- **`native-only-animated-view.tsx`** — Utility for native-only animation surfaces. Not a UI primitive; no story written. If it ever becomes user-facing, add a story per workflow rules.
diff --git a/apps/mobile/components/ui/accordion.stories.tsx b/apps/mobile/components/ui/accordion.stories.tsx
new file mode 100644
index 00000000000..eb26458d642
--- /dev/null
+++ b/apps/mobile/components/ui/accordion.stories.tsx
@@ -0,0 +1,101 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Text } from "@/components/ui/text";
+
+function AccordionShowcase({
+ type,
+ collapsible,
+}: {
+ type: "single" | "multiple";
+ collapsible: boolean;
+}) {
+ if (type === "single") {
+ return (
+
+
+
+ How do I sign in?
+
+
+
+ Open the More tab and tap "Sign in with email" — a magic link will
+ be sent to your inbox.
+
+
+
+
+
+ What is a workspace?
+
+
+
+ A workspace is a branch on a host. Each session is bound to one
+ workspace.
+
+
+
+
+
+ Why is the host offline?
+
+
+
+ The host-service process may not be running. Check that desktop
+ Superset is open and connected.
+
+
+
+
+ );
+ }
+ return (
+
+
+
+ First (independent)
+
+
+ Independently expandable.
+
+
+
+
+ Second (independent)
+
+
+ Also independent.
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Accordion",
+ component: AccordionShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Show/hide content groups. Used for FAQ-style content in Help / About screens. Choose `single` for radio behavior, `multiple` for independent toggles.",
+ },
+ },
+ },
+ args: { type: "single", collapsible: true },
+ argTypes: {
+ type: { control: { type: "select" }, options: ["single", "multiple"] },
+ collapsible: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const SingleCollapsible: Story = {};
+export const Multiple: Story = { args: { type: "multiple" } };
diff --git a/apps/mobile/components/ui/alert-dialog.stories.tsx b/apps/mobile/components/ui/alert-dialog.stories.tsx
new file mode 100644
index 00000000000..ddc9794ff70
--- /dev/null
+++ b/apps/mobile/components/ui/alert-dialog.stories.tsx
@@ -0,0 +1,95 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import { Button } from "@/components/ui/button";
+import { Text } from "@/components/ui/text";
+
+function AlertDialogShowcase({
+ triggerLabel,
+ title,
+ description,
+ confirmLabel,
+ cancelLabel,
+}: {
+ triggerLabel: string;
+ title: string;
+ description: string;
+ confirmLabel: string;
+ cancelLabel: string;
+}) {
+ return (
+
+
+
+ {triggerLabel}
+
+
+
+
+ {title}
+ {description}
+
+
+
+ {cancelLabel}
+
+
+ {confirmLabel}
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/AlertDialog",
+ component: AlertDialogShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Confirmation modal for destructive actions. Used by Delete session dialog (UC-SESS-05). Renders via portal — PortalHost is wired in preview decorator.",
+ },
+ },
+ },
+ args: {
+ triggerLabel: "Delete session",
+ title: "Delete this session?",
+ description:
+ "This will permanently remove the session and its messages. This cannot be undone.",
+ confirmLabel: "Delete",
+ cancelLabel: "Cancel",
+ },
+ argTypes: {
+ triggerLabel: { control: "text" },
+ title: { control: "text" },
+ description: { control: "text" },
+ confirmLabel: { control: "text" },
+ cancelLabel: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const DeleteSession: Story = {};
+
+export const SignOut: Story = {
+ args: {
+ triggerLabel: "Sign out",
+ title: "Sign out of Superset?",
+ description: "You'll need to sign in again to access your sessions.",
+ confirmLabel: "Sign out",
+ },
+};
diff --git a/apps/mobile/components/ui/alert.stories.tsx b/apps/mobile/components/ui/alert.stories.tsx
new file mode 100644
index 00000000000..f3960eb081c
--- /dev/null
+++ b/apps/mobile/components/ui/alert.stories.tsx
@@ -0,0 +1,97 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import {
+ AlertTriangle,
+ Info as InfoIcon,
+ type LucideIcon,
+ WifiOff,
+} from "lucide-react-native";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+
+const ICON_MAP: Record = {
+ Info: InfoIcon,
+ WifiOff,
+ AlertTriangle,
+};
+
+function AlertShowcase({
+ variant,
+ icon,
+ title,
+ description,
+}: {
+ variant: "default" | "destructive";
+ icon: keyof typeof ICON_MAP;
+ title: string;
+ description: string;
+}) {
+ return (
+
+ {title}
+ {description}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Alert",
+ component: AlertShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Inline informational/destructive alert with leading icon. Used for host-offline banner, permission-denied banner, dispatch-outcome variants (UC-PLATF-03).",
+ },
+ },
+ },
+ args: {
+ variant: "default",
+ icon: "Info",
+ title: "Host reconnected",
+ description: "Streaming has resumed.",
+ },
+ argTypes: {
+ variant: {
+ control: { type: "select" },
+ options: ["default", "destructive"],
+ },
+ icon: {
+ control: { type: "select" },
+ options: ["Info", "WifiOff", "AlertTriangle"],
+ },
+ title: { control: "text" },
+ description: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const InfoBanner: Story = {};
+
+export const HostOffline: Story = {
+ args: {
+ variant: "destructive",
+ icon: "WifiOff",
+ title: "Host offline",
+ description: "Tap to retry connecting.",
+ },
+};
+
+export const PlanUpgrade: Story = {
+ args: {
+ variant: "destructive",
+ icon: "AlertTriangle",
+ title: "Plan upgrade required",
+ description: "Your host requires a paid plan to dispatch.",
+ },
+};
+
+export const DispatchFailed: Story = {
+ args: {
+ variant: "destructive",
+ icon: "AlertTriangle",
+ title: "Host dispatch failed",
+ description: "Tap retry, or open another session.",
+ },
+};
diff --git a/apps/mobile/components/ui/aspect-ratio.stories.tsx b/apps/mobile/components/ui/aspect-ratio.stories.tsx
new file mode 100644
index 00000000000..535f9c54765
--- /dev/null
+++ b/apps/mobile/components/ui/aspect-ratio.stories.tsx
@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { AspectRatio } from "@/components/ui/aspect-ratio";
+import { Text } from "@/components/ui/text";
+
+function AspectRatioShowcase({ ratio }: { ratio: number }) {
+ return (
+
+
+
+
+ {ratio.toFixed(3)} ratio
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/AspectRatio",
+ component: AspectRatioShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Constrain a container to a fixed width:height ratio. Used for image placeholders, banners, and any layout that needs a stable proportional box.",
+ },
+ },
+ },
+ args: { ratio: 16 / 9 },
+ argTypes: {
+ ratio: {
+ control: { type: "select" },
+ options: [16 / 9, 4 / 3, 1, 3 / 4, 9 / 16],
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Widescreen: Story = { args: { ratio: 16 / 9 } };
+export const Standard: Story = { args: { ratio: 4 / 3 } };
+export const Square: Story = { args: { ratio: 1 } };
+export const Portrait: Story = { args: { ratio: 3 / 4 } };
diff --git a/apps/mobile/components/ui/avatar.stories.tsx b/apps/mobile/components/ui/avatar.stories.tsx
new file mode 100644
index 00000000000..d073bdfe639
--- /dev/null
+++ b/apps/mobile/components/ui/avatar.stories.tsx
@@ -0,0 +1,93 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Text } from "@/components/ui/text";
+
+function AvatarShowcase({
+ src,
+ fallback,
+ size,
+}: {
+ src?: string;
+ fallback: string;
+ size: "sm" | "md" | "lg" | "xl";
+}) {
+ const sizeClass = {
+ sm: "size-6",
+ md: "size-8",
+ lg: "size-12",
+ xl: "size-16",
+ }[size];
+
+ return (
+
+
+ {src ? : null}
+
+
+ {fallback}
+
+
+
+
+ {size}
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Avatar",
+ component: AvatarShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'Round avatar with optional image + fallback initial(s). Used for assistant message head ("A") and any user/identity surface. Sized variants supported via className.',
+ },
+ },
+ },
+ args: {
+ fallback: "A",
+ size: "md",
+ },
+ argTypes: {
+ src: { control: "text" },
+ fallback: { control: "text" },
+ size: {
+ control: { type: "select" },
+ options: ["sm", "md", "lg", "xl"],
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const FallbackOnly: Story = {
+ args: { fallback: "A" },
+};
+
+export const WithImage: Story = {
+ args: {
+ src: "https://i.pravatar.cc/100?img=12",
+ fallback: "JR",
+ },
+};
+
+export const Small: Story = { args: { size: "sm", fallback: "S" } };
+export const Large: Story = { args: { size: "lg", fallback: "L" } };
+export const ExtraLarge: Story = { args: { size: "xl", fallback: "XL" } };
+
+export const AssistantHead: Story = {
+ args: { fallback: "A", size: "md" },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Canonical chat usage — left-side head on assistant message (UC-RENDER-01).",
+ },
+ },
+ },
+};
diff --git a/apps/mobile/components/ui/badge.stories.tsx b/apps/mobile/components/ui/badge.stories.tsx
new file mode 100644
index 00000000000..4144dcf61a9
--- /dev/null
+++ b/apps/mobile/components/ui/badge.stories.tsx
@@ -0,0 +1,108 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Badge } from "@/components/ui/badge";
+import { Text } from "@/components/ui/text";
+
+type Variant = "default" | "secondary" | "destructive" | "outline";
+
+function BadgeShowcase({
+ variant,
+ label,
+}: {
+ variant: Variant;
+ label: string;
+}) {
+ return (
+
+ {label}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Badge",
+ component: BadgeShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'Compact rounded label. Used for "new" tags on model options, `·N` filter count, "1 of N" approval counter. Pill component (chat-domain) handles different semantics — see Components/Pill.',
+ },
+ },
+ },
+ args: {
+ variant: "default",
+ label: "new",
+ },
+ argTypes: {
+ variant: {
+ control: { type: "select" },
+ options: ["default", "secondary", "destructive", "outline"],
+ },
+ label: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const NewModelTag: Story = {
+ args: { label: "new" },
+ parameters: {
+ docs: {
+ description: {
+ story: "On Opus 4.7 row in the model picker popover (UC-COMP-04 §A).",
+ },
+ },
+ },
+};
+
+export const FilterCount: Story = {
+ args: { variant: "secondary", label: "3" },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "`·N` filter count on FilterButton when activeFilters.length ≥ 1.",
+ },
+ },
+ },
+};
+
+export const ApprovalCounter: Story = {
+ args: { variant: "outline", label: "1 of 3" },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Multi-approval counter in PendingApprovalFooter (UC-PAUSE-01 §A).",
+ },
+ },
+ },
+};
+
+export const Destructive: Story = {
+ args: { variant: "destructive", label: "failed" },
+};
+
+export const AllVariants: Story = {
+ render: () => (
+
+
+ default
+
+
+ secondary
+
+
+ destructive
+
+
+ outline
+
+
+ ),
+};
diff --git a/apps/mobile/components/ui/button.stories.tsx b/apps/mobile/components/ui/button.stories.tsx
new file mode 100644
index 00000000000..7a251415c4d
--- /dev/null
+++ b/apps/mobile/components/ui/button.stories.tsx
@@ -0,0 +1,161 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { Send } from "lucide-react-native";
+import { View } from "react-native";
+import { Button } from "@/components/ui/button";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+
+type Variant =
+ | "default"
+ | "destructive"
+ | "outline"
+ | "secondary"
+ | "ghost"
+ | "link";
+type Size = "default" | "sm" | "lg" | "icon";
+
+function ButtonShowcase({
+ variant,
+ size,
+ disabled,
+ label,
+ leadingIcon,
+}: {
+ variant: Variant;
+ size: Size;
+ disabled: boolean;
+ label: string;
+ leadingIcon: boolean;
+}) {
+ if (size === "icon") {
+ return (
+
+
+
+ );
+ }
+ return (
+
+ {leadingIcon ? : null}
+ {label}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Button",
+ component: ButtonShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Primary tappable action. 6 variants (default/destructive/outline/secondary/ghost/link) × 4 sizes (default/sm/lg/icon). Default fills with ember (--color-primary). Pressable from RN — long-press supported via consumer onLongPress.",
+ },
+ },
+ },
+ args: {
+ variant: "default",
+ size: "default",
+ disabled: false,
+ label: "Send",
+ leadingIcon: false,
+ },
+ argTypes: {
+ variant: {
+ control: { type: "select" },
+ options: [
+ "default",
+ "destructive",
+ "outline",
+ "secondary",
+ "ghost",
+ "link",
+ ],
+ },
+ size: {
+ control: { type: "select" },
+ options: ["default", "sm", "lg", "icon"],
+ },
+ disabled: { control: "boolean" },
+ label: { control: "text" },
+ leadingIcon: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Destructive: Story = {
+ args: { variant: "destructive", label: "Delete session" },
+};
+
+export const Outline: Story = {
+ args: { variant: "outline", label: "Cancel" },
+};
+
+export const Secondary: Story = {
+ args: { variant: "secondary", label: "Reject" },
+};
+
+export const Ghost: Story = {
+ args: { variant: "ghost", label: "Skip" },
+};
+
+export const Link: Story = {
+ args: { variant: "link", label: "Open in settings" },
+};
+
+export const WithLeadingIcon: Story = {
+ args: { leadingIcon: true, label: "Send" },
+};
+
+export const IconOnly: Story = {
+ args: { size: "icon" },
+ parameters: {
+ docs: {
+ description: {
+ story: "Square button for icon-only actions (Send/Stop/Close).",
+ },
+ },
+ },
+};
+
+export const SmallApprove: Story = {
+ args: { size: "sm", label: "Approve" },
+};
+
+export const LargePrimary: Story = {
+ args: { size: "lg", label: "Enable notifications" },
+};
+
+export const Disabled: Story = {
+ args: { disabled: true, label: "Send" },
+};
+
+export const AllVariants: Story = {
+ render: () => (
+
+
+ Default (ember)
+
+
+ Destructive
+
+
+ Outline
+
+
+ Secondary
+
+
+ Ghost
+
+
+ Link
+
+
+ ),
+};
diff --git a/apps/mobile/components/ui/card.stories.tsx b/apps/mobile/components/ui/card.stories.tsx
new file mode 100644
index 00000000000..730f2294068
--- /dev/null
+++ b/apps/mobile/components/ui/card.stories.tsx
@@ -0,0 +1,98 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Text } from "@/components/ui/text";
+
+function CardShowcase({
+ title,
+ description,
+ body,
+ showFooter,
+}: {
+ title: string;
+ description: string;
+ body: string;
+ showFooter: boolean;
+}) {
+ return (
+
+
+ {title}
+ {description}
+
+
+ {body}
+
+ {showFooter ? (
+
+
+
+ Cancel
+
+
+ Save
+
+
+
+ ) : null}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Card",
+ component: CardShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Elevated content container. Base for tool-call cards, pending-approval cards, info panels. Composes Header/Title/Description/Content/Footer sub-components.",
+ },
+ },
+ },
+ args: {
+ title: "Tool call",
+ description: "ReadFile · src/handlers/chat.ts",
+ body: "Reading file contents to determine the right insertion point...",
+ showFooter: false,
+ },
+ argTypes: {
+ title: { control: "text" },
+ description: { control: "text" },
+ body: { control: "text" },
+ showFooter: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithFooter: Story = { args: { showFooter: true } };
+
+export const ToolCall: Story = {
+ args: {
+ title: "ReadFile",
+ description: "src/handlers/chat.ts · 1.2KB",
+ body: "Read 47 lines · returned 0 results",
+ showFooter: false,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Tool-call card base composition (UC-RENDER-04). Full ToolCallBlock molecule adds status rule + status icon.",
+ },
+ },
+ },
+};
diff --git a/apps/mobile/components/ui/checkbox.stories.tsx b/apps/mobile/components/ui/checkbox.stories.tsx
new file mode 100644
index 00000000000..72e0c9c735c
--- /dev/null
+++ b/apps/mobile/components/ui/checkbox.stories.tsx
@@ -0,0 +1,60 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+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 (
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Checkbox",
+ component: CheckboxShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Multi-select box. Used in workspace filter rows (UC-NAV-08) and inline markdown task lists.",
+ },
+ },
+ },
+ args: {
+ initialChecked: false,
+ disabled: false,
+ label: "main · macbook-pro",
+ },
+ argTypes: {
+ initialChecked: { control: "boolean" },
+ disabled: { control: "boolean" },
+ label: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Unchecked: Story = {};
+export const Checked: Story = { args: { initialChecked: true } };
+export const Disabled: Story = {
+ args: { disabled: true, label: "main · macbook-pro (offline host)" },
+};
diff --git a/apps/mobile/components/ui/collapsible.stories.tsx b/apps/mobile/components/ui/collapsible.stories.tsx
new file mode 100644
index 00000000000..00f35fe51db
--- /dev/null
+++ b/apps/mobile/components/ui/collapsible.stories.tsx
@@ -0,0 +1,81 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { ChevronDown, ChevronRight } from "lucide-react-native";
+import { useState } from "react";
+import { Pressable, View } from "react-native";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+
+function CollapsibleShowcase({
+ initialOpen,
+ header,
+ body,
+}: {
+ initialOpen: boolean;
+ header: string;
+ body: string;
+}) {
+ const [open, setOpen] = useState(initialOpen);
+ return (
+
+
+
+
+ {header}
+
+
+
+
+
+ {body}
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Collapsible",
+ component: CollapsibleShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Show/hide a content region with a header. Used by PlanBlock + ReasoningBlock (UC-RENDER-05), tool-call argument preview, expandable feedback in PlanReviewScreen.",
+ },
+ },
+ },
+ args: {
+ initialOpen: false,
+ header: "📦 Plan",
+ body: "1. Investigate the failing test\n2. Add a fix\n3. Re-run the suite",
+ },
+ argTypes: {
+ initialOpen: { control: "boolean" },
+ header: { control: "text" },
+ body: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Collapsed: Story = {};
+export const Expanded: Story = { args: { initialOpen: true } };
+
+export const ReasoningBlock: Story = {
+ args: {
+ initialOpen: true,
+ header: "💭 Reasoning",
+ body: "The user mentioned the slash command popover keeps closing on focus loss. The issue is likely the BottomSheet capture phase swallowing the focus event...",
+ },
+};
diff --git a/apps/mobile/components/ui/context-menu.stories.tsx b/apps/mobile/components/ui/context-menu.stories.tsx
new file mode 100644
index 00000000000..5f161f71299
--- /dev/null
+++ b/apps/mobile/components/ui/context-menu.stories.tsx
@@ -0,0 +1,53 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuTrigger,
+} from "@/components/ui/context-menu";
+import { Text } from "@/components/ui/text";
+
+function ContextMenuShowcase() {
+ return (
+
+
+
+ Long-press this card
+
+
+
+
+ Rename
+
+
+ End session
+
+
+
+ Delete
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/ContextMenu",
+ component: ContextMenuShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Long-press action menu. Used for session-row long-press (Rename/End/Delete). Mobile uses native long-press; menu renders via portal.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const SessionRowActions: Story = {};
diff --git a/apps/mobile/components/ui/dialog.stories.tsx b/apps/mobile/components/ui/dialog.stories.tsx
new file mode 100644
index 00000000000..81ac3c1e33b
--- /dev/null
+++ b/apps/mobile/components/ui/dialog.stories.tsx
@@ -0,0 +1,80 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Text } from "@/components/ui/text";
+
+function DialogShowcase({
+ triggerLabel,
+ title,
+ description,
+ confirmLabel,
+}: {
+ triggerLabel: string;
+ title: string;
+ description: string;
+ confirmLabel: string;
+}) {
+ return (
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Dialog",
+ component: DialogShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Non-destructive modal. Used for Rename session, settings forms. Has built-in X close affordance top-right (44pt hitSlop). Renders via portal.",
+ },
+ },
+ },
+ args: {
+ triggerLabel: "Rename session",
+ title: "Rename session",
+ description: "Choose a clear title that helps you find this session later.",
+ confirmLabel: "Save",
+ },
+ argTypes: {
+ triggerLabel: { control: "text" },
+ title: { control: "text" },
+ description: { control: "text" },
+ confirmLabel: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const RenameSession: Story = {};
diff --git a/apps/mobile/components/ui/dropdown-menu.stories.tsx b/apps/mobile/components/ui/dropdown-menu.stories.tsx
new file mode 100644
index 00000000000..b23542bf724
--- /dev/null
+++ b/apps/mobile/components/ui/dropdown-menu.stories.tsx
@@ -0,0 +1,62 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { MoreVertical } from "lucide-react-native";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+
+function DropdownMenuShowcase() {
+ return (
+
+
+
+
+
+
+
+
+
+ Session actions
+
+
+
+
+ Rename
+
+
+ End session
+
+
+
+ Delete
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/DropdownMenu",
+ component: DropdownMenuShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Trigger-anchored menu. Used by session overflow `···` (UC-SESS-04 §A). Renders via portal.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const SessionOverflow: Story = {};
diff --git a/apps/mobile/components/ui/hover-card.stories.tsx b/apps/mobile/components/ui/hover-card.stories.tsx
new file mode 100644
index 00000000000..8d6f93c3808
--- /dev/null
+++ b/apps/mobile/components/ui/hover-card.stories.tsx
@@ -0,0 +1,45 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import {
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+} from "@/components/ui/hover-card";
+import { Text } from "@/components/ui/text";
+
+function HoverCardShowcase() {
+ return (
+
+
+ @justin
+
+
+
+ Justin Rich
+
+ Joined March 2025 · 12 active sessions
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/HoverCard",
+ component: HoverCardShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Hover/long-press preview card. On mobile, triggers via long-press (no hover). Used for mention preview, link preview. Renders via portal.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/components/ui/icon.stories.tsx b/apps/mobile/components/ui/icon.stories.tsx
new file mode 100644
index 00000000000..0297d1f984b
--- /dev/null
+++ b/apps/mobile/components/ui/icon.stories.tsx
@@ -0,0 +1,134 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import {
+ AlertTriangle,
+ ArrowDown,
+ ArrowLeft,
+ ArrowUpRight,
+ Bell,
+ Check,
+ ChevronDown,
+ ChevronRight,
+ Circle,
+ Copy,
+ GitBranch,
+ Laptop,
+ type LucideIcon,
+ MoreVertical,
+ Package,
+ Send,
+ Settings,
+ Shield,
+ Sparkles,
+ Square,
+ WifiOff,
+ X,
+ Zap,
+} from "lucide-react-native";
+import { ScrollView, View } from "react-native";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+
+const CATALOG: { name: string; icon: LucideIcon; usage: string }[] = [
+ { name: "Send", icon: Send, usage: "Composer Send button" },
+ { name: "Square", icon: Square, usage: "Composer Stop button" },
+ { name: "X", icon: X, usage: "Close affordances" },
+ { name: "ArrowLeft", icon: ArrowLeft, usage: "Back navigation" },
+ { name: "MoreVertical", icon: MoreVertical, usage: "Session overflow ···" },
+ { name: "Copy", icon: Copy, usage: "Copy code block / message" },
+ { name: "Check", icon: Check, usage: "Approve / selected state" },
+ { name: "Circle", icon: Circle, usage: "Status dot base" },
+ {
+ name: "ChevronDown",
+ icon: ChevronDown,
+ usage: "Picker triggers · expanded",
+ },
+ { name: "ChevronRight", icon: ChevronRight, usage: "Collapsed sections" },
+ { name: "ArrowDown", icon: ArrowDown, usage: "Scroll-back FAB" },
+ { name: "ArrowUpRight", icon: ArrowUpRight, usage: "External link" },
+ { name: "Package", icon: Package, usage: "📦 Plan block" },
+ { name: "GitBranch", icon: GitBranch, usage: "🌿 Workspace branch" },
+ { name: "Laptop", icon: Laptop, usage: "💻 Host (desktop)" },
+ { name: "Bell", icon: Bell, usage: "🔔 Push notification prompt" },
+ { name: "Shield", icon: Shield, usage: "🔐 Permission mode" },
+ { name: "Zap", icon: Zap, usage: "⚡ Thinking level" },
+ { name: "Settings", icon: Settings, usage: "⚙ Filter button" },
+ { name: "WifiOff", icon: WifiOff, usage: "Host offline banner" },
+ { name: "AlertTriangle", icon: AlertTriangle, usage: "Warning banners" },
+ { name: "Sparkles", icon: Sparkles, usage: "AI / generated content" },
+];
+
+function IconCatalogShowcase({
+ colorClass,
+ sizeClass,
+}: {
+ colorClass: string;
+ sizeClass: string;
+}) {
+ return (
+
+
+ Click an icon name in the catalog to inspect. All icons are
+ lucide-react-native, themed via the Icon wrapper (size + color
+ className).
+
+
+ {CATALOG.map(({ name, icon, usage }) => (
+
+
+
+
+
+ {name}
+
+ {usage}
+
+
+
+ ))}
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Icon",
+ component: IconCatalogShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Themed wrapper around lucide-react-native via withUniwind. Catalog covers chat-view icon usage. Add new icons by importing from lucide-react-native and passing via `as` prop — no per-icon wrappers needed.",
+ },
+ },
+ },
+ args: {
+ colorClass: "text-foreground",
+ sizeClass: "size-5",
+ },
+ argTypes: {
+ colorClass: {
+ control: { type: "select" },
+ options: [
+ "text-foreground",
+ "text-muted-foreground",
+ "text-primary",
+ "text-destructive",
+ "text-green-600",
+ "text-amber-600",
+ ],
+ },
+ sizeClass: {
+ control: { type: "select" },
+ options: ["size-3", "size-4", "size-5", "size-6", "size-8"],
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Catalog: Story = {};
+export const Ember: Story = { args: { colorClass: "text-primary" } };
+export const Large: Story = { args: { sizeClass: "size-8" } };
+export const Live: Story = { args: { colorClass: "text-green-600" } };
diff --git a/apps/mobile/components/ui/input.stories.tsx b/apps/mobile/components/ui/input.stories.tsx
new file mode 100644
index 00000000000..d1d3293f9ae
--- /dev/null
+++ b/apps/mobile/components/ui/input.stories.tsx
@@ -0,0 +1,86 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useState } from "react";
+import { View } from "react-native";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+function InputShowcase({
+ placeholder,
+ editable,
+ initialValue,
+ showLabel,
+}: {
+ placeholder: string;
+ editable: boolean;
+ initialValue: string;
+ showLabel: boolean;
+}) {
+ const [value, setValue] = useState(initialValue);
+ return (
+
+ {showLabel ? : null}
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Input",
+ component: InputShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Single-line text input. Used by SessionSearchBar (UC-NAV-07), plan-review feedback field (UC-PAUSE-03).",
+ },
+ },
+ },
+ args: {
+ placeholder: "Search this project's sessions",
+ editable: true,
+ initialValue: "",
+ showLabel: false,
+ },
+ argTypes: {
+ placeholder: { control: "text" },
+ editable: { control: "boolean" },
+ initialValue: { control: "text" },
+ showLabel: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Empty: Story = {};
+
+export const WithValue: Story = {
+ args: { initialValue: "host-service refactor" },
+};
+
+export const WithLabel: Story = { args: { showLabel: true } };
+
+export const Disabled: Story = {
+ args: { editable: false, initialValue: "Cannot edit" },
+};
+
+export const FeedbackField: Story = {
+ args: {
+ showLabel: true,
+ placeholder: "Add feedback...",
+ initialValue: "",
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: "Plan-review feedback input (UC-PAUSE-03).",
+ },
+ },
+ },
+};
diff --git a/apps/mobile/components/ui/label.stories.tsx b/apps/mobile/components/ui/label.stories.tsx
new file mode 100644
index 00000000000..37b16aabfd1
--- /dev/null
+++ b/apps/mobile/components/ui/label.stories.tsx
@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Label } from "@/components/ui/label";
+
+function LabelShowcase({
+ text,
+ disabled,
+}: {
+ text: string;
+ disabled: boolean;
+}) {
+ return (
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Label",
+ component: LabelShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Form-control label — typically pairs with an Input, Switch, RadioGroup, or Checkbox. Reads --color-foreground with disabled opacity treatment.",
+ },
+ },
+ },
+ args: {
+ text: "Workspace name",
+ disabled: false,
+ },
+ argTypes: {
+ text: { control: "text" },
+ disabled: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const Disabled: Story = {
+ args: { disabled: true, text: "Workspace (locked)" },
+};
diff --git a/apps/mobile/components/ui/menubar.stories.tsx b/apps/mobile/components/ui/menubar.stories.tsx
new file mode 100644
index 00000000000..3720da0a8d2
--- /dev/null
+++ b/apps/mobile/components/ui/menubar.stories.tsx
@@ -0,0 +1,68 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useState } from "react";
+import {
+ Menubar,
+ MenubarContent,
+ MenubarItem,
+ MenubarMenu,
+ MenubarSeparator,
+ MenubarTrigger,
+} from "@/components/ui/menubar";
+import { Text } from "@/components/ui/text";
+
+function MenubarShowcase() {
+ const [open, setOpen] = useState(undefined);
+ return (
+
+
+
+ File
+
+
+
+ New session
+
+
+ Open recent
+
+
+
+ Sign out
+
+
+
+
+
+ Edit
+
+
+
+ Undo
+
+
+ Redo
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Menubar",
+ component: MenubarShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Multi-trigger menu bar. Primarily a desktop pattern — included for parity with shadcn ecosystem; mobile chat does not currently use this surface.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/components/ui/popover.stories.tsx b/apps/mobile/components/ui/popover.stories.tsx
new file mode 100644
index 00000000000..039eb78f10e
--- /dev/null
+++ b/apps/mobile/components/ui/popover.stories.tsx
@@ -0,0 +1,64 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Text } from "@/components/ui/text";
+
+function PopoverShowcase({
+ triggerLabel,
+ side,
+}: {
+ triggerLabel: string;
+ side: "top" | "bottom";
+}) {
+ return (
+
+
+
+ {triggerLabel}
+
+
+
+
+ Model picker
+
+ Opus 4.7 · Sonnet 4.6 · Haiku 4.5 · GPT-5.5
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Popover",
+ component: PopoverShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Anchored floating panel. Base for slash-command-popover (UC-COMP-01 §C), model picker (UC-COMP-04), thinking-level picker (UC-COMP-05). Renders via portal.",
+ },
+ },
+ },
+ args: { triggerLabel: "Sonnet 4.6 ▾", side: "top" },
+ argTypes: {
+ triggerLabel: { control: "text" },
+ side: {
+ control: { type: "select" },
+ options: ["top", "bottom"],
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const Above: Story = { args: { side: "top" } };
+export const Below: Story = { args: { side: "bottom" } };
diff --git a/apps/mobile/components/ui/progress.stories.tsx b/apps/mobile/components/ui/progress.stories.tsx
new file mode 100644
index 00000000000..b469fcedff3
--- /dev/null
+++ b/apps/mobile/components/ui/progress.stories.tsx
@@ -0,0 +1,71 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useEffect, useState } from "react";
+import { View } from "react-native";
+import { Progress } from "@/components/ui/progress";
+import { Text } from "@/components/ui/text";
+
+function ProgressShowcase({
+ value,
+ animated,
+}: {
+ value: number;
+ animated: boolean;
+}) {
+ const [v, setV] = useState(value);
+ useEffect(() => {
+ if (!animated) {
+ setV(value);
+ return;
+ }
+ setV(0);
+ const id = setInterval(() => {
+ setV((current) => {
+ if (current >= 100) return 0;
+ return current + 5;
+ });
+ }, 200);
+ return () => clearInterval(id);
+ }, [animated, value]);
+
+ return (
+
+
+
+ {Math.round(v)}%
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Progress",
+ component: ProgressShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Linear progress bar. Used during slash-command preview loading (UC-COMP-01 §C), long-running tool calls. Spring-animated on native.",
+ },
+ },
+ },
+ args: {
+ value: 35,
+ animated: false,
+ },
+ argTypes: {
+ value: { control: { type: "range", min: 0, max: 100, step: 5 } },
+ animated: {
+ control: "boolean",
+ description: "Auto-cycles 0→100 on a 4s loop for animation inspection",
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Static: Story = {};
+export const Half: Story = { args: { value: 50 } };
+export const Full: Story = { args: { value: 100 } };
+export const Animating: Story = { args: { animated: true } };
diff --git a/apps/mobile/components/ui/radio-group.stories.tsx b/apps/mobile/components/ui/radio-group.stories.tsx
new file mode 100644
index 00000000000..12f75673b76
--- /dev/null
+++ b/apps/mobile/components/ui/radio-group.stories.tsx
@@ -0,0 +1,83 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useState } from "react";
+import { View } from "react-native";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { Text } from "@/components/ui/text";
+
+function RadioGroupShowcase({
+ options,
+ initialValue,
+}: {
+ options: { value: string; label: string; meta?: string }[];
+ initialValue: string;
+}) {
+ const [value, setValue] = useState(initialValue);
+ return (
+
+ {options.map((opt) => (
+
+
+
+
+ {opt.meta ? (
+
+ {opt.meta}
+
+ ) : null}
+
+
+ ))}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/RadioGroup",
+ component: RadioGroupShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Single-select picker. Used for model picker rows (UC-COMP-04), thinking-level rows (UC-COMP-05), permission-mode rows.",
+ },
+ },
+ },
+ args: {
+ options: [
+ {
+ value: "low",
+ label: "Low",
+ meta: "~1K tokens · fastest, less reasoning depth",
+ },
+ {
+ value: "medium",
+ label: "Medium",
+ meta: "~4K tokens · balanced",
+ },
+ {
+ value: "high",
+ label: "High",
+ meta: "~12K tokens · deep reasoning, slower",
+ },
+ {
+ value: "xhigh",
+ label: "X-High",
+ meta: "~32K tokens · maximum reasoning",
+ },
+ ],
+ initialValue: "medium",
+ },
+ argTypes: {
+ initialValue: {
+ control: { type: "select" },
+ options: ["low", "medium", "high", "xhigh"],
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const ThinkingLevels: Story = {};
diff --git a/apps/mobile/components/ui/select.stories.tsx b/apps/mobile/components/ui/select.stories.tsx
new file mode 100644
index 00000000000..988661d82e0
--- /dev/null
+++ b/apps/mobile/components/ui/select.stories.tsx
@@ -0,0 +1,59 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+function SelectShowcase() {
+ return (
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Select",
+ component: SelectShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Native-feeling select dropdown. Used for inline form selects in settings. Composer model picker uses Popover + radio rows for visual richness instead — but Select is appropriate for simple key-value choices.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const ModelPicker: Story = {};
diff --git a/apps/mobile/components/ui/separator.stories.tsx b/apps/mobile/components/ui/separator.stories.tsx
new file mode 100644
index 00000000000..d56b60700ab
--- /dev/null
+++ b/apps/mobile/components/ui/separator.stories.tsx
@@ -0,0 +1,58 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Separator } from "@/components/ui/separator";
+import { Text } from "@/components/ui/text";
+
+function SeparatorShowcase({
+ orientation,
+}: {
+ orientation: "horizontal" | "vertical";
+}) {
+ if (orientation === "vertical") {
+ return (
+
+ Left
+
+ Right
+
+ );
+ }
+ return (
+
+ Above the line
+
+
+ Below the line
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Separator",
+ component: SeparatorShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Hairline divider — 1px line using --color-border. Horizontal (default) or vertical. Used in popovers, message gaps, sheet sections.",
+ },
+ },
+ },
+ args: {
+ orientation: "horizontal",
+ },
+ argTypes: {
+ orientation: {
+ control: { type: "select" },
+ options: ["horizontal", "vertical"],
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Horizontal: Story = {};
+export const Vertical: Story = { args: { orientation: "vertical" } };
diff --git a/apps/mobile/components/ui/skeleton.stories.tsx b/apps/mobile/components/ui/skeleton.stories.tsx
new file mode 100644
index 00000000000..820d969d0dc
--- /dev/null
+++ b/apps/mobile/components/ui/skeleton.stories.tsx
@@ -0,0 +1,83 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Skeleton } from "@/components/ui/skeleton";
+
+function SkeletonShowcase({
+ pattern,
+}: {
+ pattern: "single" | "message-list" | "row";
+}) {
+ if (pattern === "single") {
+ return ;
+ }
+ if (pattern === "row") {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Skeleton",
+ component: SkeletonShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Loading placeholder with pulsing opacity (Reanimated). Used during chat history fetch (UC-SESS-02 §A), avatar load, and any deferred content.",
+ },
+ },
+ },
+ args: {
+ pattern: "single",
+ },
+ argTypes: {
+ pattern: {
+ control: { type: "select" },
+ options: ["single", "row", "message-list"],
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const SingleBar: Story = { args: { pattern: "single" } };
+export const RowWithAvatar: Story = { args: { pattern: "row" } };
+export const ChatHistoryLoading: Story = {
+ args: { pattern: "message-list" },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Skeleton variant for UC-SESS-02 §A — alternating bubble shapes mimicking user/assistant pattern.",
+ },
+ },
+ },
+};
diff --git a/apps/mobile/components/ui/switch.stories.tsx b/apps/mobile/components/ui/switch.stories.tsx
new file mode 100644
index 00000000000..220655a49d1
--- /dev/null
+++ b/apps/mobile/components/ui/switch.stories.tsx
@@ -0,0 +1,63 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useState } from "react";
+import { View } from "react-native";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+
+function SwitchShowcase({
+ initialChecked,
+ disabled,
+ label,
+}: {
+ initialChecked: boolean;
+ disabled: boolean;
+ label: string;
+}) {
+ const [checked, setChecked] = useState(initialChecked);
+ return (
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Switch",
+ component: SwitchShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Boolean toggle. Used in settings rows (Re-enable in Settings, notification preferences). Thumb translates 14px on check; bg flips to --color-primary (ember) when on.",
+ },
+ },
+ },
+ args: {
+ initialChecked: false,
+ disabled: false,
+ label: "Push notifications",
+ },
+ argTypes: {
+ initialChecked: { control: "boolean" },
+ disabled: { control: "boolean" },
+ label: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Off: Story = {};
+export const On: Story = { args: { initialChecked: true } };
+export const DisabledOff: Story = {
+ args: { disabled: true, label: "Notifications (locked by admin)" },
+};
+export const DisabledOn: Story = {
+ args: { disabled: true, initialChecked: true, label: "Always pinned" },
+};
diff --git a/apps/mobile/components/ui/tabs.stories.tsx b/apps/mobile/components/ui/tabs.stories.tsx
new file mode 100644
index 00000000000..680b772e5e8
--- /dev/null
+++ b/apps/mobile/components/ui/tabs.stories.tsx
@@ -0,0 +1,64 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useState } from "react";
+import { View } from "react-native";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Text } from "@/components/ui/text";
+
+function TabsShowcase() {
+ const [value, setValue] = useState("streaming");
+ return (
+
+
+
+ Streaming
+
+
+ Pause pending
+
+
+ Idle
+
+
+
+
+
+ 3 sessions currently streaming.
+
+
+
+
+
+
+ 1 session waiting for input.
+
+
+
+
+
+
+ 12 idle sessions.
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Tabs",
+ component: TabsShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Tab segment + content. Used for inline status filtering, settings categories.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const SessionStatusFilter: Story = {};
diff --git a/apps/mobile/components/ui/text.stories.tsx b/apps/mobile/components/ui/text.stories.tsx
new file mode 100644
index 00000000000..456cb22874a
--- /dev/null
+++ b/apps/mobile/components/ui/text.stories.tsx
@@ -0,0 +1,96 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Text } from "@/components/ui/text";
+
+type Variant =
+ | "default"
+ | "h1"
+ | "h2"
+ | "h3"
+ | "h4"
+ | "p"
+ | "blockquote"
+ | "code"
+ | "lead"
+ | "large"
+ | "small"
+ | "muted";
+
+function TextShowcase({
+ variant,
+ children,
+}: {
+ variant: Variant;
+ children: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Text",
+ component: TextShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Typed text primitive with 12 variants (default, h1-h4, p, blockquote, code, lead, large, small, muted). The catalog view of all variants is in Design System/Typography — this story is for per-variant inspection.",
+ },
+ },
+ },
+ args: {
+ variant: "default",
+ children: "The quick brown fox jumps over the lazy dog.",
+ },
+ argTypes: {
+ variant: {
+ control: { type: "select" },
+ options: [
+ "default",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "p",
+ "blockquote",
+ "code",
+ "lead",
+ "large",
+ "small",
+ "muted",
+ ],
+ },
+ children: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const H1: Story = { args: { variant: "h1", children: "Page title" } };
+export const H2: Story = { args: { variant: "h2", children: "Section" } };
+export const H3: Story = { args: { variant: "h3", children: "Sub-section" } };
+export const Paragraph: Story = {
+ args: {
+ variant: "p",
+ children:
+ "Multi-sentence paragraph with leading-7 spacing. Used inside markdown rendering and any prose region.",
+ },
+};
+export const Code: Story = {
+ args: { variant: "code", children: "const ember = '#e07850';" },
+};
+export const Muted: Story = {
+ args: { variant: "muted", children: "Secondary metadata text" },
+};
+export const Blockquote: Story = {
+ args: {
+ variant: "blockquote",
+ children: "Italic blockquote with left border.",
+ },
+};
diff --git a/apps/mobile/components/ui/textarea.stories.tsx b/apps/mobile/components/ui/textarea.stories.tsx
new file mode 100644
index 00000000000..b21cb18cb47
--- /dev/null
+++ b/apps/mobile/components/ui/textarea.stories.tsx
@@ -0,0 +1,88 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+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 (
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Textarea",
+ component: TextareaShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Multi-line text input — composer body, ask_user BottomSheetTextInput, plan-review feedback. Autogrows up to numberOfLines (max height) on native.",
+ },
+ },
+ },
+ args: {
+ placeholder: "Send a message...",
+ editable: true,
+ initialValue: "",
+ numberOfLines: 8,
+ },
+ argTypes: {
+ placeholder: { control: "text" },
+ editable: { control: "boolean" },
+ initialValue: { control: "text" },
+ numberOfLines: { control: { type: "number", min: 1, max: 20 } },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Empty: Story = {};
+
+export const WithValue: Story = {
+ args: {
+ initialValue:
+ "Please review the chat-mobile-sprint-1 worktree and confirm the ember tokens are wired through correctly. Pay particular attention to dark-mode contrast on the assistant message bubble.",
+ },
+};
+
+export const Composer: Story = {
+ args: { placeholder: "Send a message..." },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Empty composer body (UC-COMP-01 §A). Real composer composes Textarea + SlashCommandNode + FileMentionNode via Tiptap WebView.",
+ },
+ },
+ },
+};
+
+export const Disabled: Story = {
+ args: { editable: false, initialValue: "Streaming — input disabled" },
+ parameters: {
+ docs: {
+ description: { story: "Disabled state during streaming (UC-COMP-03)." },
+ },
+ },
+};
diff --git a/apps/mobile/components/ui/toggle-group.stories.tsx b/apps/mobile/components/ui/toggle-group.stories.tsx
new file mode 100644
index 00000000000..466b1ef8a24
--- /dev/null
+++ b/apps/mobile/components/ui/toggle-group.stories.tsx
@@ -0,0 +1,61 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { useState } from "react";
+import { Text } from "@/components/ui/text";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
+
+function ToggleGroupShowcase({ type }: { type: "single" | "multiple" }) {
+ const [single, setSingle] = useState("medium");
+ const [multi, setMulti] = useState(["streaming"]);
+ if (type === "single") {
+ return (
+
+
+ Low
+
+
+ Medium
+
+
+ High
+
+
+ );
+ }
+ return (
+
+
+ Streaming
+
+
+ Pause
+
+
+ Idle
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/ToggleGroup",
+ component: ToggleGroupShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Segmented control. Single (radio-style) or multiple (checkbox-style). Status filter in SessionFilterSheet uses this pattern.",
+ },
+ },
+ },
+ args: { type: "single" },
+ argTypes: {
+ type: { control: { type: "select" }, options: ["single", "multiple"] },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Single: Story = {};
+export const Multiple: Story = { args: { type: "multiple" } };
diff --git a/apps/mobile/components/ui/toggle.stories.tsx b/apps/mobile/components/ui/toggle.stories.tsx
new file mode 100644
index 00000000000..85fd7688561
--- /dev/null
+++ b/apps/mobile/components/ui/toggle.stories.tsx
@@ -0,0 +1,62 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { Bold } from "lucide-react-native";
+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 (
+
+ {withIcon ? : null}
+ {label}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Toggle",
+ component: ToggleShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Single on/off button. Single-cell variant of ToggleGroup. Useful for icon-style boolean controls (favorite, pin, etc.).",
+ },
+ },
+ },
+ args: {
+ variant: "default",
+ withIcon: false,
+ label: "Bold",
+ initialPressed: false,
+ },
+ argTypes: {
+ variant: {
+ control: { type: "select" },
+ options: ["default", "outline"],
+ },
+ withIcon: { control: "boolean" },
+ label: { control: "text" },
+ initialPressed: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const Pressed: Story = { args: { initialPressed: true } };
+export const WithIcon: Story = { args: { withIcon: true } };
+export const Outline: Story = { args: { variant: "outline" } };
diff --git a/apps/mobile/components/ui/tooltip.stories.tsx b/apps/mobile/components/ui/tooltip.stories.tsx
new file mode 100644
index 00000000000..451a44dea22
--- /dev/null
+++ b/apps/mobile/components/ui/tooltip.stories.tsx
@@ -0,0 +1,55 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { Info } from "lucide-react-native";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+function TooltipShowcase({
+ side,
+ content,
+}: {
+ side: "top" | "bottom";
+ content: string;
+}) {
+ return (
+
+
+
+
+
+ {content}
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/Tooltip",
+ component: TooltipShowcase,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Long-press hint on mobile. Used sparingly — chat view's long-press affordances mostly use ContextMenu (system menu) instead. Renders via portal.",
+ },
+ },
+ },
+ args: {
+ side: "top",
+ content: "Sessions are scoped to the selected project.",
+ },
+ argTypes: {
+ side: { control: { type: "select" }, options: ["top", "bottom"] },
+ content: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js
index beb8c9486c1..7a56f770a2b 100644
--- a/apps/mobile/metro.config.js
+++ b/apps/mobile/metro.config.js
@@ -1,5 +1,6 @@
const { getDefaultConfig } = require("expo/metro-config");
const { withUniwindConfig } = require("uniwind/metro");
+const withStorybook = require("@storybook/react-native/metro/withStorybook");
const path = require("node:path");
const projectRoot = __dirname;
@@ -7,24 +8,25 @@ const monorepoRoot = path.resolve(projectRoot, "../..");
const config = getDefaultConfig(projectRoot);
-// Watch all files in the monorepo
config.watchFolders = [monorepoRoot];
-// Let Metro find modules from the monorepo root
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(monorepoRoot, "node_modules"),
];
-// Enable package exports for better-auth
config.resolver.unstable_enablePackageExports = true;
-// Resolve local Expo Modules (modules/ dir)
config.resolver.extraNodeModules = {
"@superset/tab-bar": path.resolve(projectRoot, "modules/tab-bar"),
};
-module.exports = withUniwindConfig(config, {
+const uniwindConfig = withUniwindConfig(config, {
cssEntryFile: "./global.css",
dtsFile: "./uniwind-types.d.ts",
});
+
+module.exports = withStorybook(uniwindConfig, {
+ configPath: path.resolve(projectRoot, ".rnstorybook"),
+ enabled: process.env.EXPO_PUBLIC_STORYBOOK === "true",
+});
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 9f30ca4c65f..1cf38ebbcc1 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -7,6 +7,8 @@
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
+ "storybook": "sb-rn-get-stories --config-path .rnstorybook && EXPO_PUBLIC_STORYBOOK=true expo start",
+ "storybook-generate": "sb-rn-get-stories --config-path .rnstorybook",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"lint:fix": "biome check --write ."
@@ -14,8 +16,11 @@
"dependencies": {
"@better-auth/expo": "1.6.5",
"@electric-sql/client": "1.5.15",
+ "@expo-google-fonts/geist": "0.4.2",
+ "@expo-google-fonts/geist-mono": "0.4.2",
"@expo/ui": "56.0.12",
"@expo/vector-icons": "15.1.1",
+ "@gorhom/bottom-sheet": "5.2.12",
"@react-native-async-storage/async-storage": "2.2.0",
"@rn-primitives/accordion": "1.4.0",
"@rn-primitives/alert-dialog": "1.4.0",
@@ -61,13 +66,16 @@
"expo-dev-client": "56.0.14",
"expo-device": "56.0.4",
"expo-file-system": "56.0.7",
+ "expo-font": "56.0.0",
"expo-glass-effect": "56.0.4",
"expo-image": "56.0.8",
+ "expo-linear-gradient": "56.0.4",
"expo-linking": "56.0.11",
"expo-localization": "56.0.6",
"expo-network": "56.0.4",
"expo-router": "56.2.5",
"expo-secure-store": "56.0.4",
+ "expo-splash-screen": "56.0.0",
"expo-status-bar": "56.0.4",
"expo-system-ui": "56.0.5",
"expo-web-browser": "56.0.5",
@@ -90,10 +98,14 @@
"zod": "4.3.6"
},
"devDependencies": {
+ "@storybook/addon-ondevice-actions": "^9",
+ "@storybook/addon-ondevice-controls": "^9",
+ "@storybook/react-native": "^9",
"@types/node": "24.12.0",
"@types/react": "19.2.14",
"babel-preset-expo": "56.0.11",
"expo-mcp": "0.2.4",
+ "storybook": "^9",
"typescript": "5.9.3"
},
"private": true
diff --git a/apps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsx b/apps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsx
new file mode 100644
index 00000000000..d62d7c43316
--- /dev/null
+++ b/apps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsx
@@ -0,0 +1,21 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { DevSignInButton } from "./DevSignInButton";
+
+const meta: Meta = {
+ title: "Components/DevSignInButton",
+ component: DevSignInButton,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Dev-only sign-in shortcut on the sign-in screen — bypasses OAuth and signs in as Local Admin (admin@local.test) for local development. Real auth call happens on press; in Storybook the button still calls into auth-client which will error harmlessly without a configured backend. Token-clean (uses Button outline variant + `text-destructive` for error). Per screens/AUDIT.md.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsx b/apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsx
new file mode 100644
index 00000000000..7172475bef0
--- /dev/null
+++ b/apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsx
@@ -0,0 +1,52 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SocialButton } from "./SocialButton";
+
+const meta: Meta = {
+ title: "Components/SocialButton",
+ component: SocialButton,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "OAuth provider button. Outline button + brand-colored SVG icon. GitHub icon uses `theme.foreground` (token-driven, fixed in audit 2026-05-22 — was previously hardcoded white/black via useColorScheme). Google icon uses official brand hex colors per Google branding requirements — documented exception in screens/AUDIT.md.",
+ },
+ },
+ },
+ args: {
+ provider: "github",
+ disabled: false,
+ },
+ argTypes: {
+ provider: {
+ control: { type: "select" },
+ options: ["github", "google"],
+ },
+ disabled: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const GitHub: Story = { args: { provider: "github" } };
+export const Google: Story = { args: { provider: "google" } };
+export const Disabled: Story = { args: { disabled: true } };
+
+export const BothProviders: Story = {
+ render: () => (
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Canonical sign-in screen composition — both providers stacked vertically.",
+ },
+ },
+ },
+};
diff --git a/apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx b/apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx
index 6d9f02a5bbf..a05a7ee8a64 100644
--- a/apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx
+++ b/apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx
@@ -1,8 +1,8 @@
-import { useColorScheme } from "react-native";
import Svg, { Path } from "react-native-svg";
import type { ButtonProps } from "@/components/ui/button";
import { Button } from "@/components/ui/button";
import { Text } from "@/components/ui/text";
+import { useTheme } from "@/hooks/useTheme";
import { cn } from "@/lib/utils";
export type SocialProvider = "github" | "google";
@@ -55,8 +55,10 @@ export function SocialButton({
className,
...props
}: SocialButtonProps) {
- const colorScheme = useColorScheme();
- const iconColor = colorScheme === "dark" ? "white" : "black";
+ // Use the active app theme (not system colorScheme) so the icon color tracks
+ // our Uniwind theme rather than the OS appearance setting.
+ const theme = useTheme();
+ const iconColor = theme.foreground;
return (
`
+ * placement, which does not render outside of an expo-router navigator context.
+ *
+ * This story renders the *inner content* of the toolbar button using the same
+ * primitives and tokens — `OrganizationAvatar` + Text + `ChevronsUpDown`. The
+ * inner layout is the audit surface; the toolbar wrapper is a positional
+ * affordance that lives in the real navigation host.
+ */
+function OrganizationHeaderButtonPreview({
+ name,
+ logo,
+ onPress,
+}: {
+ name?: string;
+ logo?: string | null;
+ onPress: () => void;
+}) {
+ const theme = useTheme();
+ return (
+
+
+
+
+ {name ?? "Organization"}
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/OrganizationHeaderButton",
+ component: OrganizationHeaderButtonPreview,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Header pressable for the workspaces screen — opens the organization switcher sheet. **Renders inside expo-router Stack.Toolbar in the real app**; this story renders the inner content only. Fixed in audit 2026-05-22 — chevron color was previously hardcoded `hsl(240 5% 64.9%)` (old cool-neutral); now resolves via `theme.mutedForeground`.",
+ },
+ },
+ },
+ args: {
+ name: "Superset",
+ logo: null,
+ onPress: () => undefined,
+ },
+ argTypes: {
+ name: { control: "text" },
+ logo: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const LongName: Story = {
+ args: { name: "Acme Long Organization Name LLC" },
+};
+export const WithLogo: Story = {
+ args: { name: "Vercel", logo: "https://logo.clearbit.com/vercel.com" },
+};
+export const NoName: Story = { args: { name: undefined } };
diff --git a/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx b/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
index 8e200db8965..a97e4079e17 100644
--- a/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
+++ b/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
@@ -2,6 +2,7 @@ import { Stack } from "expo-router";
import { ChevronsUpDown } from "lucide-react-native";
import { Pressable } from "react-native";
import { Text } from "@/components/ui/text";
+import { useTheme } from "@/hooks/useTheme";
import { OrganizationAvatar } from "../OrganizationSwitcherSheet/components/OrganizationAvatar";
export function OrganizationHeaderButton({
@@ -13,6 +14,7 @@ export function OrganizationHeaderButton({
logo?: string | null;
onPress: () => void;
}) {
+ const theme = useTheme();
return (
<>
@@ -22,7 +24,7 @@ export function OrganizationHeaderButton({
{name ?? "Organization"}
-
+
diff --git a/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsx b/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsx
new file mode 100644
index 00000000000..4f6536061be
--- /dev/null
+++ b/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsx
@@ -0,0 +1,164 @@
+import Ionicons from "@expo/vector-icons/Ionicons";
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { Platform, Pressable, View } from "react-native";
+import { Text } from "@/components/ui/text";
+import { useTheme } from "@/hooks/useTheme";
+import { OrganizationAvatar } from "./components/OrganizationAvatar";
+import { OrganizationSwitcherSheet } from "./OrganizationSwitcherSheet";
+
+const MOCK_ORGS = [
+ {
+ id: "org_1",
+ name: "Superset",
+ slug: "superset",
+ logo: null,
+ },
+ {
+ id: "org_2",
+ name: "Acme Corp",
+ slug: "acme",
+ logo: null,
+ },
+ {
+ id: "org_3",
+ name: "Vercel",
+ slug: "vercel",
+ logo: "https://logo.clearbit.com/vercel.com",
+ },
+];
+
+/**
+ * The real `OrganizationSwitcherSheet` uses `@expo/ui/swift-ui` — an iOS-only
+ * SwiftUI bridge that does not render on Android Emulator inside Storybook.
+ *
+ * On iOS, the actual sheet appears as a native bottom sheet over the rest of
+ * the UI. On Android (and in static previews), this story renders the *inner
+ * content layout* of the sheet using the same tokens.
+ */
+function OrganizationSwitcherSheetInner({
+ organizations,
+ activeOrganizationId,
+}: {
+ organizations: typeof MOCK_ORGS;
+ activeOrganizationId: string;
+}) {
+ const theme = useTheme();
+ return (
+
+
+ Organizations
+
+ {organizations.map((organization) => {
+ const isActive = organization.id === activeOrganizationId;
+ return (
+
+
+
+
+ {organization.name}
+
+ {organization.slug ? (
+
+ superset.sh/{organization.slug}
+
+ ) : null}
+
+ {isActive ? (
+
+ ) : null}
+
+ );
+ })}
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/OrganizationSwitcherSheet",
+ component: OrganizationSwitcherSheetInner,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Bottom sheet for switching organizations. **iOS-only in production** — uses `@expo/ui/swift-ui` (Host/BottomSheet/Group/RNHostView). Inner layout shown here in Storybook; real sheet appears as a native SwiftUI bottom sheet over the app. The sheet forces `colorScheme: dark` (documented exception in screens/AUDIT.md).",
+ },
+ },
+ },
+ args: {
+ organizations: MOCK_ORGS,
+ activeOrganizationId: "org_1",
+ },
+ argTypes: {
+ activeOrganizationId: {
+ control: { type: "select" },
+ options: MOCK_ORGS.map((o) => o.id),
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const ThreeOrgs: Story = {};
+export const SuperLongList: Story = {
+ args: {
+ organizations: [
+ ...MOCK_ORGS,
+ { id: "org_4", name: "Initech", slug: "initech", logo: null },
+ { id: "org_5", name: "Cyberdyne", slug: "cyberdyne", logo: null },
+ { id: "org_6", name: "Stark Industries", slug: "stark", logo: null },
+ { id: "org_7", name: "Wayne Enterprises", slug: "wayne", logo: null },
+ ],
+ },
+};
+export const Active2nd: Story = { args: { activeOrganizationId: "org_2" } };
+
+/**
+ * Real-component story — only renders meaningfully on iOS. Renders nothing on
+ * Android/web; use the InnerLayout stories above for visual inspection.
+ */
+export const NativeSheet: Story = {
+ render: () => {
+ if (Platform.OS !== "ios") {
+ return (
+
+
+ iOS-only — switch to iOS Simulator to render the real SwiftUI
+ BottomSheet.
+
+
+ );
+ }
+ return (
+ undefined}
+ organizations={MOCK_ORGS}
+ activeOrganizationId="org_1"
+ onSwitchOrganization={() => undefined}
+ width={390}
+ />
+ );
+ },
+};
diff --git a/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsx b/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsx
new file mode 100644
index 00000000000..1778bc1b871
--- /dev/null
+++ b/apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsx
@@ -0,0 +1,61 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { Text } from "@/components/ui/text";
+import { OrganizationAvatar } from "./OrganizationAvatar";
+
+const meta: Meta = {
+ title: "Components/OrganizationAvatar",
+ component: OrganizationAvatar,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Organization avatar — renders the org logo if present, otherwise a token-themed initial fallback (`theme.muted` bg, `theme.mutedForeground` text). Used in tab-bar accessory, header button, switcher sheet rows. Token-compliant per screens/AUDIT.md.",
+ },
+ },
+ },
+ args: {
+ name: "Superset",
+ logo: null,
+ size: 32,
+ },
+ argTypes: {
+ name: { control: "text" },
+ logo: { control: "text" },
+ size: { control: { type: "range", min: 16, max: 96, step: 4 } },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const FallbackSmall: Story = { args: { size: 24 } };
+export const FallbackMedium: Story = { args: { size: 32 } };
+export const FallbackLarge: Story = { args: { size: 64 } };
+export const FallbackTwoWordName: Story = { args: { name: "Acme Corp" } };
+export const FallbackNoName: Story = { args: { name: undefined } };
+
+export const WithLogo: Story = {
+ args: {
+ name: "Vercel",
+ logo: "https://logo.clearbit.com/vercel.com",
+ },
+};
+
+export const SizeMatrix: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+ Sizes: 20 · 28 · 36 · 48 · 64 px
+
+
+ ),
+};
diff --git a/apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsx b/apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsx
new file mode 100644
index 00000000000..e10a659e429
--- /dev/null
+++ b/apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsx
@@ -0,0 +1,76 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { Platform, View } from "react-native";
+import { Text } from "@/components/ui/text";
+
+/**
+ * The real `AuthenticatedTabBar` mounts a `TabBarView` from `@superset/tab-bar`
+ * — a SwiftUI-bridged native tab bar — driven by `expo-router/ui` hooks
+ * (`useTabTrigger`, `useRouter`) and the `useOrganizations` collection query.
+ * None of those render in Storybook isolation:
+ *
+ * - `TabBarView` is a native bridge that needs the live navigation host
+ * - `useTabTrigger` requires an active expo-router Tabs context
+ * - `useOrganizations` queries the Electric collection provider
+ *
+ * This story is a **dependency-acknowledgement placeholder** — it documents
+ * the integration surface and points to the running app for visual verification.
+ * The audit target is purely layout (StyleSheet.create with `position: absolute`
+ * + `height: 96`); no token consumption locally. Per screens/AUDIT.md (CLEAN).
+ */
+function AuthenticatedTabBarPlaceholder() {
+ return (
+
+ AuthenticatedTabBar
+
+ Native tab bar — verified in running app, not in Storybook isolation.
+
+
+
+
+ Dependencies that prevent isolation rendering:
+
+
+ · `@superset/tab-bar` TabBarView (SwiftUI bridge)
+
+
+ · `useTabTrigger` from expo-router/ui (requires Tabs context)
+
+
+ · `useOrganizations` (Electric collection query)
+
+
+
+
+ Local audit surface (StyleSheet):
+
+ · `position: 'absolute'` · `height: 96` · no color literals
+
+
+ Token-clean per screens/AUDIT.md — no remediation needed.
+
+
+
+ Platform.OS = {Platform.OS}
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/AuthenticatedTabBar",
+ component: AuthenticatedTabBarPlaceholder,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Authenticated bottom tab bar wrapping `@superset/tab-bar` TabBarView (SwiftUI bridge). **Not renderable in Storybook isolation** — the native tab bar and expo-router context can only be exercised in the running app. This placeholder documents the integration surface; token audit (CLEAN) is in screens/AUDIT.md.",
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Placeholder: Story = {};
diff --git a/apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx b/apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx
new file mode 100644
index 00000000000..96f9d671b1e
--- /dev/null
+++ b/apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx
@@ -0,0 +1,122 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import {
+ ArrowLeftRight,
+ ChevronDown,
+ LogOut,
+ Settings,
+ UserPlus,
+} from "lucide-react-native";
+import { View } from "react-native";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+
+/**
+ * The real `OrgDropdown` depends on the full auth + Electric collections stack
+ * (authClient.useSession, useLiveQuery on collections.organizations,
+ * useSignOut, useCollections). Wiring those in Storybook would require a
+ * fully-mocked QueryClient + collection provider — out of scope for a story.
+ *
+ * This story renders the *visual surface* using the same primitives and tokens
+ * as the real component, with mock org data. The token audit is on this
+ * layout. The data integration is verified in the running app.
+ */
+function OrgDropdownPreview({
+ activeOrgName,
+ switchableOrgs,
+ disabled,
+}: {
+ activeOrgName: string;
+ switchableOrgs: string[];
+ disabled: boolean;
+}) {
+ const orgInitial = activeOrgName.charAt(0).toUpperCase();
+ return (
+
+
+
+
+
+ {orgInitial}
+
+
+
+
+ {activeOrgName}
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+ Invite members
+
+ {switchableOrgs.length > 0 ? : null}
+ {switchableOrgs.map((name) => (
+
+
+ Switch to {name}
+
+ ))}
+ {switchableOrgs.length > 0 ? : null}
+
+
+ Log out
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/OrgDropdown",
+ component: OrgDropdownPreview,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Authenticated header dropdown — shows active org, Settings, Invite members, switchable orgs, Log out. Real component reads via `authClient.useSession()` + `useLiveQuery(collections.organizations)`; this Storybook surface uses mock data for visual audit. Token-clean (Tailwind classes only; no hardcoded values). Per screens/AUDIT.md.",
+ },
+ },
+ },
+ args: {
+ activeOrgName: "Superset",
+ switchableOrgs: ["Acme Corp", "Vercel"],
+ disabled: false,
+ },
+ argTypes: {
+ activeOrgName: { control: "text" },
+ disabled: { control: "boolean" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const SingleOrgNoSwitch: Story = { args: { switchableOrgs: [] } };
+export const ManySwitchableOrgs: Story = {
+ args: {
+ switchableOrgs: ["Acme Corp", "Vercel", "Initech", "Cyberdyne", "Stark"],
+ },
+};
+export const SwitchingInProgress: Story = { args: { disabled: true } };
+export const LongOrgName: Story = {
+ args: {
+ activeOrgName: "Very Long Organization Name That Truncates",
+ },
+};
diff --git a/apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsx b/apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsx
new file mode 100644
index 00000000000..a962069fa71
--- /dev/null
+++ b/apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsx
@@ -0,0 +1,84 @@
+import Ionicons from "@expo/vector-icons/Ionicons";
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { ChevronsUpDown } from "lucide-react-native";
+import { Pressable, View } from "react-native";
+import { Text } from "@/components/ui/text";
+import { useTheme } from "@/hooks/useTheme";
+import { OrganizationAvatar } from "@/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar";
+
+/**
+ * The real `TabBarAccessory` depends on the `useOrganizations` hook (Electric
+ * collection query) + `useRouter` (expo-router) + `useWindowDimensions`. The
+ * latter two are infrastructure that don't render in Storybook; the former
+ * requires a mounted collection provider.
+ *
+ * This story renders the *inner row layout* using the same primitives and
+ * tokens, with mock organization data. The audit target is the row; the
+ * `OrganizationSwitcherSheet` it opens is a separate story.
+ */
+function TabBarAccessoryPreview({
+ orgName,
+ orgLogo,
+}: {
+ orgName: string;
+ orgLogo: string | null;
+}) {
+ const theme = useTheme();
+ return (
+
+
+
+
+ {orgName}
+
+
+
+
+
+
+
+ );
+}
+
+const meta: Meta = {
+ title: "Components/TabBarAccessory",
+ component: TabBarAccessoryPreview,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Tab-bar accessory row — org switcher trigger (left) + settings gear (right). Lives above the tab bar in authenticated routes. Real component pulls org from `useOrganizations` and opens `OrganizationSwitcherSheet` on org tap. Token-clean: colors via `useTheme()`, layout via Tailwind. Per screens/AUDIT.md.",
+ },
+ },
+ },
+ args: {
+ orgName: "Superset",
+ orgLogo: null,
+ },
+ argTypes: {
+ orgName: { control: "text" },
+ orgLogo: { control: "text" },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+export const WithLogo: Story = {
+ args: {
+ orgName: "Vercel",
+ orgLogo: "https://logo.clearbit.com/vercel.com",
+ },
+};
+export const LongOrgName: Story = {
+ args: { orgName: "Acme Corporation International" },
+};
diff --git a/apps/mobile/screens/AUDIT.md b/apps/mobile/screens/AUDIT.md
new file mode 100644
index 00000000000..b4be728bd38
--- /dev/null
+++ b/apps/mobile/screens/AUDIT.md
@@ -0,0 +1,99 @@
+# App Component Token-Compliance Audit
+
+**Generated:** 2026-05-22 (Path A ember migration verification)
+**Scope:** First-party app components in `apps/mobile/screens/**/components/` and `apps/mobile/screens/*/components/`
+**Companion doc:** `apps/mobile/components/ui/AUDIT.md` (vendor primitives — separate immutability rule)
+
+This audit covers the 8 live app components used by the mobile app's authenticated and unauthenticated routes. Unlike the vendor `components/ui/*` primitives, **these are first-party code** — token-bypass findings here are fixed in place rather than escalated to upstream.
+
+---
+
+## Audit pass — 2026-05-22
+
+### ✅ Clean (6/8) — no remediation needed
+
+| Component | Reason |
+|---|---|
+| `AuthenticatedTabBar.tsx` | Pure layout via `StyleSheet.create` (position + height); colors flow through `@superset/tab-bar`. No tokens consumed locally. |
+| `TabBarAccessory.tsx` | All colors via `useTheme()` (`theme.foreground`, `theme.mutedForeground`); Tailwind classes (`px-4`, `text-sm`) are token-driven. |
+| `OrgDropdown.tsx` | Pure Tailwind composition. Reads `text-muted-foreground`, `text-foreground`, `text-destructive`, `size-9` — all theme-routed. |
+| `DevSignInButton.tsx` | Composes vendor `Button` + `Text`. `text-destructive` is the only token reference, correctly used. |
+| `OrganizationAvatar.tsx` | All colors via `useTheme()` (`theme.muted`, `theme.mutedForeground`). |
+| `OrganizationSwitcherSheet.tsx` | Most colors via `useTheme()`. Has one documented exception below. |
+
+### 🛠 Fixed in this audit (2/8)
+
+#### `OrganizationHeaderButton.tsx` — CRITICAL fix
+
+**Before (line 25):**
+```tsx
+
+```
+
+The hex/HSL value `hsl(240 5% 64.9%)` references the OLD cool-neutral palette (HSL 240 hue family) that was removed under Path A. Under the new warm ember theme this rendered as a cool-gray chevron sitting on warm-neutral surfaces — visually wrong even at first paint.
+
+**After:**
+```tsx
+const theme = useTheme();
+// ...
+
+```
+
+Now resolves to the warm `hsl(15 4% 65%)` from `lib/theme.ts` THEME.dark.mutedForeground. Tracks the theme automatically.
+
+#### `SocialButton.tsx` — MEDIUM fix
+
+**Before:**
+```tsx
+const colorScheme = useColorScheme();
+const iconColor = colorScheme === "dark" ? "white" : "black";
+```
+
+Two problems:
+1. **Hardcoded `white`/`black` literals** — bypass the token system entirely.
+2. **`useColorScheme()` reads the OS appearance setting**, not our Uniwind theme. Since `Uniwind.setTheme("dark")` is called unconditionally in `screens/RootLayout/RootLayout.tsx`, the SocialButton icon could disagree with the active theme if a user has light-mode iOS but the app is forced dark — the icon would render black-on-dark, illegible.
+
+**After:**
+```tsx
+const theme = useTheme();
+const iconColor = theme.foreground;
+```
+
+Now `iconColor` tracks the active Uniwind theme via the THEME object. Tokenized.
+
+### 📋 Documented exceptions (kept as-is)
+
+#### `SocialButton.tsx` Google icon brand colors
+
+```tsx
+ {/* Google blue */}
+ {/* Google green */}
+ {/* Google yellow */}
+ {/* Google red */}
+```
+
+**Decision:** keep. These are official Google brand colors per [Google's branding guidelines](https://about.google/brand-resource-center/) — recoloring would violate brand requirements. Pinned hex literals are the correct treatment for branded assets.
+
+#### `OrganizationSwitcherSheet.tsx` forced dark colorScheme
+
+```tsx
+modifiers={[
+ environment("colorScheme", "dark"),
+ presentationDragIndicator("visible"),
+ background(theme.background),
+]}
+```
+
+**Decision:** keep. The SwiftUI `BottomSheet` is wrapped in `environment("colorScheme", "dark")`, forcing dark appearance regardless of system color scheme. This is intentional — the sheet uses the ember dark surface tokens (`theme.background = hsl(13 16% 7%)`) for all themes since `Uniwind.setTheme("dark")` is the app default. Flagging here so it's visible at audit time; revisit if/when the app supports system-driven light/dark switching.
+
+#### Storybook caveat for `OrganizationSwitcherSheet`
+
+The sheet uses `@expo/ui/swift-ui` (`Host`, `BottomSheet`, `Group`, `RNHostView`) — these are **iOS-only SwiftUI bridges**. On Android Emulator inside Storybook, this component will not render meaningfully (the SwiftUI bridge is a no-op). Its story acknowledges this and renders an alternate stub on non-iOS.
+
+---
+
+## Verification surface
+
+All 8 components have `Components/{Name}` Storybook stories at sidecar paths (e.g., `screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx`). The Storybook glob in `.rnstorybook/main.js` was extended to include `../screens/**/*.stories.?(ts|tsx|js|jsx)` so these stories are picked up.
+
+Use the Storybook walkthrough on iOS Simulator + Android Emulator to verify every component renders correctly under ember.
diff --git a/apps/mobile/screens/sessions-list/README.md b/apps/mobile/screens/sessions-list/README.md
new file mode 100644
index 00000000000..10177c325cd
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/README.md
@@ -0,0 +1,46 @@
+# Sessions-list (pixel-perfect Wave 5)
+
+Storybook-only sessions-list view layer. Each subfolder under `views/`
+corresponds 1:1 with a design in `designs/views/01-sessions-list/`.
+
+## What lives here
+
+| Path | Purpose |
+|---|---|
+| `components/{SessionsList, ProjectPickerSheet, SessionFilterSheet, NewChatSheet}/` | 4 organisms specific to this tier |
+| `views/{ViewName}/` | 10 view stories (1 loaded + 5 empty + 1 contact-sheet + 3 overlays) |
+| `types.ts` | Domain type re-exports + ChatSession / Project / WorkspacePickerEntry / SessionsFilters / SessionsEmptyVariant |
+| `mock-data.ts` | Centralized fixtures matching `designs/views/01-sessions-list/states/loaded/README.md` |
+
+## Wireframe ↔ view mapping (10 views, UC-NAV)
+
+| View | UC |
+|---|---|
+| SessionsListLoaded | UC-NAV §A |
+| SessionsListEmptyNoProjects | UC-NAV-06.1 |
+| SessionsListEmptyNoWorkspaces | UC-NAV-06.2 |
+| SessionsListEmptyNoSessions | UC-NAV-06.3 |
+| SessionsListSearchNoMatch | UC-NAV-06.4 |
+| SessionsListFiltersNoMatch | UC-NAV-06.5 |
+| SessionsListCombinedEmpty | reference contact sheet (all 5 variants stacked) |
+| ProjectPickerSheetView | UC-NAV §B |
+| SessionFilterSheetView | UC-NAV-08 §C |
+| NewChatSheetView | UC-NAV §D |
+
+## New molecules (this wave)
+
+`SessionRow`, `ProjectChipHeader`, `EmptyState`, `FilterCheckboxRow`,
+`AppliedFilterTag`, `WorkspacePickerRow`, `ProjectPickerRow` — all under
+`apps/mobile/components/`.
+
+## Storybook integration
+
+`apps/mobile/.rnstorybook/main.js` gets a new narrow glob:
+
+```js
+"../screens/sessions-list/**/*.stories.?(ts|tsx|js|jsx)"
+```
+
+Same pattern as chat-view — narrow because sibling screens import
+expo-router which crashes Storybook 9 RN's prep-time `loadStory`. Files
+under `sessions-list/` stay router-free by construction.
diff --git a/apps/mobile/screens/sessions-list/components/NewChatSheet/NewChatSheet.tsx b/apps/mobile/screens/sessions-list/components/NewChatSheet/NewChatSheet.tsx
new file mode 100644
index 00000000000..1fa8a7cd7c8
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/NewChatSheet/NewChatSheet.tsx
@@ -0,0 +1,72 @@
+import { BottomSheetScrollView, BottomSheetView } from "@gorhom/bottom-sheet";
+import { Sparkles, X } from "lucide-react-native";
+import { forwardRef } from "react";
+import { View } from "react-native";
+import { BottomSheet, type BottomSheetRef } from "@/components/BottomSheet";
+import { IconButton } from "@/components/IconButton";
+import { Icon } from "@/components/ui/icon";
+import { Text } from "@/components/ui/text";
+import { WorkspacePickerRow } from "@/components/WorkspacePickerRow";
+import type { WorkspacePickerEntry } from "../../types";
+
+export type NewChatSheetProps = {
+ projectName: string;
+ workspaces: ReadonlyArray;
+ onWorkspaceSelect?: (workspace: WorkspacePickerEntry) => void;
+ onClose?: () => void;
+};
+
+/**
+ * New-chat workspace picker bottom sheet (UC-NAV §D). Renders the workspaces
+ * for the active project so the user can pick one to start a new session in.
+ *
+ * Composes BottomSheet + WorkspacePickerRow. Empty workspaces (`sessionCount`
+ * = 0) are still listed.
+ */
+export const NewChatSheet = forwardRef(
+ function NewChatSheet(
+ { projectName, workspaces, onWorkspaceSelect, onClose },
+ ref,
+ ) {
+ return (
+
+
+
+
+
+
+ Start a new chat
+
+
+
+
+
+ Pick a workspace in {projectName}
+
+
+ {workspaces.map((w) => (
+ onWorkspaceSelect?.(w)}
+ />
+ ))}
+
+
+
+ );
+ },
+);
diff --git a/apps/mobile/screens/sessions-list/components/NewChatSheet/index.ts b/apps/mobile/screens/sessions-list/components/NewChatSheet/index.ts
new file mode 100644
index 00000000000..4bbd2e0b635
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/NewChatSheet/index.ts
@@ -0,0 +1 @@
+export { NewChatSheet, type NewChatSheetProps } from "./NewChatSheet";
diff --git a/apps/mobile/screens/sessions-list/components/ProjectPickerSheet/ProjectPickerSheet.tsx b/apps/mobile/screens/sessions-list/components/ProjectPickerSheet/ProjectPickerSheet.tsx
new file mode 100644
index 00000000000..2a8e6f66dfb
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/ProjectPickerSheet/ProjectPickerSheet.tsx
@@ -0,0 +1,73 @@
+import { BottomSheetView } from "@gorhom/bottom-sheet";
+import { X } from "lucide-react-native";
+import { forwardRef } from "react";
+import { View } from "react-native";
+import { BottomSheet, type BottomSheetRef } from "@/components/BottomSheet";
+import { IconButton } from "@/components/IconButton";
+import { ProjectPickerRow } from "@/components/ProjectPickerRow";
+import { Text } from "@/components/ui/text";
+import type { Project } from "../../types";
+
+export type ProjectPickerSheetProps = {
+ projects: ReadonlyArray;
+ selectedProjectId: string;
+ onProjectSelect?: (project: Project) => void;
+ onClose?: () => void;
+};
+
+/**
+ * Project picker bottom sheet (UC-NAV §B). Renders the project list in a
+ * 50% snap-point sheet over the dimmed sessions list.
+ *
+ * Composes BottomSheet + ProjectPickerRow. Imperative API via ref:
+ * const sheetRef = useRef(null);
+ * sheetRef.current?.present();
+ */
+export const ProjectPickerSheet = forwardRef<
+ BottomSheetRef,
+ ProjectPickerSheetProps
+>(function ProjectPickerSheet(
+ { projects, selectedProjectId, onProjectSelect, onClose },
+ ref,
+) {
+ return (
+
+
+
+
+ Switch project
+
+
+
+
+
+ This organization
+
+
+ {projects.map((p) => (
+ onProjectSelect?.(p)}
+ />
+ ))}
+
+
+
+ );
+});
diff --git a/apps/mobile/screens/sessions-list/components/ProjectPickerSheet/index.ts b/apps/mobile/screens/sessions-list/components/ProjectPickerSheet/index.ts
new file mode 100644
index 00000000000..84c3438c921
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/ProjectPickerSheet/index.ts
@@ -0,0 +1,4 @@
+export {
+ ProjectPickerSheet,
+ type ProjectPickerSheetProps,
+} from "./ProjectPickerSheet";
diff --git a/apps/mobile/screens/sessions-list/components/SessionFilterSheet/SessionFilterSheet.tsx b/apps/mobile/screens/sessions-list/components/SessionFilterSheet/SessionFilterSheet.tsx
new file mode 100644
index 00000000000..7525ecf050b
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/SessionFilterSheet/SessionFilterSheet.tsx
@@ -0,0 +1,135 @@
+import { BottomSheetScrollView, BottomSheetView } from "@gorhom/bottom-sheet";
+import { X } from "lucide-react-native";
+import { forwardRef, useState } from "react";
+import { View } from "react-native";
+import { BottomSheet, type BottomSheetRef } from "@/components/BottomSheet";
+import {
+ FilterCheckboxRow,
+ type FilterStatusValue,
+} from "@/components/FilterCheckboxRow";
+import { IconButton } from "@/components/IconButton";
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
+import { Text } from "@/components/ui/text";
+import type { FilterValueWorkspace, SessionsFilters } from "../../types";
+
+const STATUS_OPTIONS: ReadonlyArray<{
+ value: FilterStatusValue;
+ label: string;
+}> = [
+ { value: "streaming", label: "Streaming" },
+ { value: "pause-pending", label: "Pause pending" },
+ { value: "idle", label: "Idle" },
+];
+
+export type SessionFilterSheetProps = {
+ workspaces: ReadonlyArray;
+ initialFilters: SessionsFilters;
+ onApply?: (next: SessionsFilters) => void;
+ onClose?: () => void;
+};
+
+/**
+ * Full filter sheet (UC-NAV-08 §C). Two stacked sections (Workspaces +
+ * Status) with multi-select FilterCheckboxRow per row, and a docked footer
+ * with Clear all / Apply buttons.
+ *
+ * Internal state stays in the sheet — only commits via `onApply` on tap.
+ */
+export const SessionFilterSheet = forwardRef<
+ BottomSheetRef,
+ SessionFilterSheetProps
+>(function SessionFilterSheet(
+ { workspaces, initialFilters, onApply, onClose },
+ ref,
+) {
+ const [workspaceIds, setWorkspaceIds] = useState>(
+ initialFilters.workspaceIds,
+ );
+ const [statuses, setStatuses] = useState>(
+ initialFilters.statuses,
+ );
+
+ const toggleWorkspace = (id: string) =>
+ setWorkspaceIds((curr) =>
+ curr.includes(id) ? curr.filter((w) => w !== id) : [...curr, id],
+ );
+
+ const toggleStatus = (value: FilterStatusValue) =>
+ setStatuses((curr) =>
+ curr.includes(value) ? curr.filter((s) => s !== value) : [...curr, value],
+ );
+
+ const clearAll = () => {
+ setWorkspaceIds([]);
+ setStatuses([]);
+ };
+
+ const apply = () => {
+ onApply?.({ workspaceIds, statuses });
+ };
+
+ return (
+
+
+
+
+ Filter sessions
+
+
+
+
+
+
+ Workspaces
+
+ {workspaces.map((w) => (
+ toggleWorkspace(w.id)}
+ />
+ ))}
+
+
+ Status
+
+ {STATUS_OPTIONS.map((o) => (
+ toggleStatus(o.value)}
+ />
+ ))}
+
+
+
+ Clear all
+
+
+ Apply
+
+
+
+
+ );
+});
diff --git a/apps/mobile/screens/sessions-list/components/SessionFilterSheet/index.ts b/apps/mobile/screens/sessions-list/components/SessionFilterSheet/index.ts
new file mode 100644
index 00000000000..250efdceb13
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/SessionFilterSheet/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionFilterSheet,
+ type SessionFilterSheetProps,
+} from "./SessionFilterSheet";
diff --git a/apps/mobile/screens/sessions-list/components/SessionsList/SessionsList.tsx b/apps/mobile/screens/sessions-list/components/SessionsList/SessionsList.tsx
new file mode 100644
index 00000000000..abbdc7eb9ee
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/SessionsList/SessionsList.tsx
@@ -0,0 +1,164 @@
+import { Plus } from "lucide-react-native";
+import type { ReactNode } from "react";
+import {
+ FlatList,
+ type FlatListProps,
+ type ListRenderItemInfo,
+ ScrollView,
+ View,
+ type ViewProps,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { AppliedFilterTag } from "@/components/AppliedFilterTag";
+import { FabBase } from "@/components/FabBase";
+import {
+ ProjectChipHeader,
+ type ProjectChipHeaderProps,
+} from "@/components/ProjectChipHeader";
+import { SessionRow } from "@/components/SessionRow";
+import { Separator } from "@/components/ui/separator";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+import type { ChatSession } from "../../types";
+
+export type SessionsListAppliedFilter = {
+ id: string;
+ kind: "workspace" | "status";
+ label: string;
+};
+
+export type SessionsListProps = ViewProps & {
+ projectName: string;
+ headerProps?: Omit;
+ sessions: ReadonlyArray;
+ appliedFilters?: ReadonlyArray;
+ onClearFilters?: () => void;
+ onFilterDismiss?: (id: string) => void;
+ onSessionPress?: (session: ChatSession) => void;
+ onSessionLongPress?: (session: ChatSession) => void;
+ onNewChatPress?: () => void;
+ /** Hide the FAB (e.g., during empty-no-workspaces). Default true. */
+ showFab?: boolean;
+ /** Empty body slot — replaces FlatList when sessions length is 0. */
+ emptyBody?: ReactNode;
+ flatListProps?: Omit<
+ FlatListProps,
+ "data" | "renderItem" | "keyExtractor"
+ >;
+};
+
+/**
+ * Sessions-list organism (UC-NAV §A). Composes:
+ * - ProjectChipHeader (two-row sticky header) with optional AppliedFilterTag
+ * row injected via the `belowSearch` slot
+ * - FlatList separated by hairlines
+ * - FabBase (NewChatFab) anchored bottom-right above the tab bar safe-area
+ *
+ * No expo-router / no useTheme — storybook-safe.
+ */
+export function SessionsList({
+ projectName,
+ headerProps,
+ sessions,
+ appliedFilters,
+ onClearFilters,
+ onFilterDismiss,
+ onSessionPress,
+ onSessionLongPress,
+ onNewChatPress,
+ showFab = true,
+ emptyBody,
+ flatListProps,
+ className,
+ ...props
+}: SessionsListProps) {
+ const insets = useSafeAreaInsets();
+ const hasFilters = (appliedFilters?.length ?? 0) > 0;
+
+ const renderItem = ({ item }: ListRenderItemInfo) => (
+ onSessionPress?.(item)}
+ onLongPress={() => onSessionLongPress?.(item)}
+ />
+ );
+
+ return (
+
+
+
+ {appliedFilters?.map((f) => (
+ onFilterDismiss?.(f.id)}
+ />
+ ))}
+ {onClearFilters ? (
+
+ ) : null}
+
+
+ ) : null
+ }
+ />
+ {sessions.length === 0 && emptyBody ? (
+ emptyBody
+ ) : (
+
+ data={sessions as ChatSession[]}
+ renderItem={renderItem}
+ keyExtractor={(s) => s.id}
+ ItemSeparatorComponent={Separator}
+ ListEmptyComponent={
+
+ No sessions
+
+ }
+ {...flatListProps}
+ />
+ )}
+ {showFab ? (
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/components/SessionsList/index.ts b/apps/mobile/screens/sessions-list/components/SessionsList/index.ts
new file mode 100644
index 00000000000..8cbc7a7ce03
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/components/SessionsList/index.ts
@@ -0,0 +1,5 @@
+export {
+ SessionsList,
+ type SessionsListAppliedFilter,
+ type SessionsListProps,
+} from "./SessionsList";
diff --git a/apps/mobile/screens/sessions-list/mock-data.ts b/apps/mobile/screens/sessions-list/mock-data.ts
new file mode 100644
index 00000000000..7ac1de2de44
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/mock-data.ts
@@ -0,0 +1,140 @@
+import type { ChatSession, Project, WorkspacePickerEntry } from "./types";
+
+/**
+ * Shared fixtures for sessions-list view stories. Mirrors the data shown in
+ * `designs/views/01-sessions-list/states/loaded/README.md`.
+ */
+
+export const MOCK_PROJECT_NAME = "superset";
+
+export const MOCK_SESSIONS: ChatSession[] = [
+ {
+ id: "s1",
+ title: "Chat-v2 design",
+ branch: "chat-mobile-plan",
+ hostName: "macbook",
+ hostKind: "laptop",
+ status: "live",
+ statusLabel: "streaming",
+ timeLabel: "2m ago",
+ },
+ {
+ id: "s2",
+ title: "Migration plan",
+ branch: "api-rewrite",
+ hostName: "cloud-1",
+ hostKind: "cloud",
+ status: "live",
+ statusLabel: "streaming",
+ timeLabel: "5m ago",
+ },
+ {
+ id: "s3",
+ title: "API cleanup",
+ branch: "chat-mobile-plan",
+ hostName: "macbook",
+ hostKind: "laptop",
+ status: "idle",
+ timeLabel: "1h ago",
+ },
+ {
+ id: "s4",
+ title: "Auth refactor",
+ branch: "main",
+ hostName: "desktop",
+ hostKind: "laptop",
+ status: "warning",
+ statusLabel: "pause pending",
+ timeLabel: "—",
+ },
+ {
+ id: "s5",
+ title: "Hot-fix backport",
+ branch: "main",
+ hostName: "desktop",
+ hostKind: "laptop",
+ status: "archived",
+ timeLabel: "1d ago",
+ },
+];
+
+export const MOCK_PROJECTS: Project[] = [
+ { id: "p1", name: "superset", workspaceCount: 4, sessionCount: 12 },
+ { id: "p2", name: "JustinCode", workspaceCount: 1, sessionCount: 2 },
+ { id: "p3", name: "LaneShadow", workspaceCount: 2, sessionCount: 0 },
+];
+
+export const MOCK_WORKSPACES_FOR_NEW_CHAT: WorkspacePickerEntry[] = [
+ {
+ id: "w1",
+ branch: "chat-mobile-plan",
+ hostName: "macbook",
+ hostKind: "laptop",
+ sessionCount: 5,
+ lastActiveTimeLabel: "2m ago",
+ },
+ {
+ id: "w2",
+ branch: "api-rewrite",
+ hostName: "cloud-1",
+ hostKind: "cloud",
+ sessionCount: 3,
+ lastActiveTimeLabel: "1h ago",
+ },
+ {
+ id: "w3",
+ branch: "main",
+ hostName: "macbook",
+ hostKind: "laptop",
+ sessionCount: 2,
+ lastActiveTimeLabel: "yesterday",
+ },
+ {
+ id: "w4",
+ branch: "main",
+ hostName: "desktop",
+ hostKind: "laptop",
+ sessionCount: 1,
+ lastActiveTimeLabel: "3 days ago",
+ },
+ {
+ id: "w5",
+ branch: "feature-x",
+ hostName: "cloud-1",
+ hostKind: "cloud",
+ sessionCount: 0,
+ },
+];
+
+export const MOCK_FILTER_WORKSPACES = [
+ {
+ id: "fw1",
+ branch: "chat-mobile-plan",
+ hostName: "macbook",
+ hostKind: "laptop" as const,
+ },
+ {
+ id: "fw2",
+ branch: "api-rewrite",
+ hostName: "cloud-1",
+ hostKind: "cloud" as const,
+ },
+ {
+ id: "fw3",
+ branch: "main",
+ hostName: "macbook",
+ hostKind: "laptop" as const,
+ },
+ {
+ id: "fw4",
+ branch: "main",
+ hostName: "desktop",
+ hostKind: "laptop" as const,
+ },
+ {
+ id: "fw5",
+ branch: "feature-x",
+ hostName: "cloud-1",
+ hostKind: "cloud" as const,
+ },
+];
diff --git a/apps/mobile/screens/sessions-list/types.ts b/apps/mobile/screens/sessions-list/types.ts
new file mode 100644
index 00000000000..52a549b6370
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/types.ts
@@ -0,0 +1,84 @@
+/**
+ * Sessions-list domain types — mirrors contracts described in
+ * plans/chat-mobile-plan/{04-uc-sess, 09-uc-nav}.md and re-exports the
+ * relevant molecule/atom prop types for view stories.
+ */
+
+export type {
+ AppliedFilterTagKind,
+ AppliedFilterTagProps,
+} from "@/components/AppliedFilterTag";
+export type { EmptyStateProps } from "@/components/EmptyState";
+export type {
+ FilterCheckboxRowKind,
+ FilterCheckboxRowProps,
+ FilterStatusValue,
+} from "@/components/FilterCheckboxRow";
+export type {
+ ProjectChipHeaderProps,
+ ProjectChipHeaderVariant,
+} from "@/components/ProjectChipHeader";
+export type { ProjectPickerRowProps } from "@/components/ProjectPickerRow";
+export type {
+ SessionHostKind,
+ SessionRowProps,
+ SessionStatus,
+} from "@/components/SessionRow";
+export type { WorkspacePickerRowProps } from "@/components/WorkspacePickerRow";
+
+/**
+ * Domain types describing the chat_sessions Electric shape rows the
+ * sessions-list reads via TanStack DB. See
+ * plans/chat-mobile-plan/11-technical-requirements/01-data-schema.md.
+ */
+export type ChatSession = {
+ id: string;
+ title: string;
+ branch: string;
+ hostName: string;
+ hostKind: import("@/components/SessionRow").SessionHostKind;
+ status: import("@/components/SessionRow").SessionStatus;
+ statusLabel?: string;
+ timeLabel: string;
+ unread?: boolean;
+};
+
+export type Project = {
+ id: string;
+ name: string;
+ workspaceCount: number;
+ sessionCount: number;
+};
+
+export type WorkspacePickerEntry = {
+ id: string;
+ branch: string;
+ hostName: string;
+ hostKind: import("@/components/SessionRow").SessionHostKind;
+ sessionCount: number;
+ lastActiveTimeLabel?: string;
+};
+
+export type FilterValueWorkspace = {
+ id: string;
+ branch: string;
+ hostName: string;
+ hostKind: import("@/components/SessionRow").SessionHostKind;
+};
+
+export type SessionsFilters = {
+ workspaceIds: ReadonlyArray;
+ statuses: ReadonlyArray<
+ import("@/components/FilterCheckboxRow").FilterStatusValue
+ >;
+};
+
+/**
+ * 5 empty-state variants per UC-NAV-06.
+ */
+export type SessionsEmptyVariant =
+ | "no-projects"
+ | "no-workspaces"
+ | "no-sessions"
+ | "search-no-match"
+ | "filters-no-match";
diff --git a/apps/mobile/screens/sessions-list/views/NewChatSheetView/NewChatSheetView.stories.tsx b/apps/mobile/screens/sessions-list/views/NewChatSheetView/NewChatSheetView.stories.tsx
new file mode 100644
index 00000000000..81abdca5608
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/NewChatSheetView/NewChatSheetView.stories.tsx
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { NewChatSheetView } from "./NewChatSheetView";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · New chat sheet",
+ component: NewChatSheetView,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV §D — new-chat workspace picker bottom sheet over dimmed sessions list. 5 workspace rows with branch · host metadata + sessions count + last-active time. Empty workspace shows 'no sessions yet'.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: { autoPresent: true },
+ argTypes: { autoPresent: { control: "boolean" } },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/NewChatSheetView/NewChatSheetView.tsx b/apps/mobile/screens/sessions-list/views/NewChatSheetView/NewChatSheetView.tsx
new file mode 100644
index 00000000000..dfad3aeb705
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/NewChatSheetView/NewChatSheetView.tsx
@@ -0,0 +1,58 @@
+import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
+import { useEffect, useRef } from "react";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
+import type { BottomSheetRef } from "@/components/BottomSheet";
+import { NewChatSheet } from "../../components/NewChatSheet";
+import { SessionsList } from "../../components/SessionsList";
+import {
+ MOCK_PROJECT_NAME,
+ MOCK_SESSIONS,
+ MOCK_WORKSPACES_FOR_NEW_CHAT,
+} from "../../mock-data";
+
+export type NewChatSheetViewProps = {
+ className?: string;
+ autoPresent?: boolean;
+};
+
+/**
+ * UC-NAV §D — new-chat workspace picker bottom sheet over dimmed sessions
+ * list. 5 workspace rows including one with no sessions yet. Tap a row to
+ * dismiss the sheet (caller would navigate to the new chat).
+ */
+export function NewChatSheetView({
+ className,
+ autoPresent = true,
+}: NewChatSheetViewProps) {
+ const sheetRef = useRef(null);
+
+ useEffect(() => {
+ if (autoPresent) sheetRef.current?.present();
+ }, [autoPresent]);
+
+ return (
+
+
+ {},
+ onProjectChipPress: () => {},
+ onFilterPress: () => {},
+ }}
+ onNewChatPress={() => sheetRef.current?.present()}
+ />
+ sheetRef.current?.dismiss()}
+ onClose={() => sheetRef.current?.dismiss()}
+ />
+
+
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/NewChatSheetView/index.ts b/apps/mobile/screens/sessions-list/views/NewChatSheetView/index.ts
new file mode 100644
index 00000000000..24c28e0d025
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/NewChatSheetView/index.ts
@@ -0,0 +1,4 @@
+export {
+ NewChatSheetView,
+ type NewChatSheetViewProps,
+} from "./NewChatSheetView";
diff --git a/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/ProjectPickerSheetView.stories.tsx b/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/ProjectPickerSheetView.stories.tsx
new file mode 100644
index 00000000000..69e3ff43eb8
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/ProjectPickerSheetView.stories.tsx
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { ProjectPickerSheetView } from "./ProjectPickerSheetView";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Project picker sheet",
+ component: ProjectPickerSheetView,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV §B — project picker bottom sheet over a dimmed SessionsList. 3 project rows with workspace + session counts. Tap a row to select (useState).",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: { autoPresent: true },
+ argTypes: { autoPresent: { control: "boolean" } },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/ProjectPickerSheetView.tsx b/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/ProjectPickerSheetView.tsx
new file mode 100644
index 00000000000..4f04ce867e7
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/ProjectPickerSheetView.tsx
@@ -0,0 +1,63 @@
+import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
+import { useEffect, useRef, useState } from "react";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
+import type { BottomSheetRef } from "@/components/BottomSheet";
+import { ProjectPickerSheet } from "../../components/ProjectPickerSheet";
+import { SessionsList } from "../../components/SessionsList";
+import {
+ MOCK_PROJECT_NAME,
+ MOCK_PROJECTS,
+ MOCK_SESSIONS,
+} from "../../mock-data";
+
+export type ProjectPickerSheetViewProps = {
+ className?: string;
+ autoPresent?: boolean;
+};
+
+/**
+ * UC-NAV §B — project picker bottom sheet open over a dimmed sessions list.
+ * Composes ProjectPickerSheet organism + SessionsList shell behind it.
+ * Tap a project row to update the selected indicator (useState).
+ */
+export function ProjectPickerSheetView({
+ className,
+ autoPresent = true,
+}: ProjectPickerSheetViewProps) {
+ const sheetRef = useRef(null);
+ const [selectedId, setSelectedId] = useState("p1");
+
+ useEffect(() => {
+ if (autoPresent) sheetRef.current?.present();
+ }, [autoPresent]);
+
+ return (
+
+
+ {},
+ onProjectChipPress: () => sheetRef.current?.present(),
+ onFilterPress: () => {},
+ }}
+ onSessionPress={() => {}}
+ onNewChatPress={() => {}}
+ />
+ {
+ setSelectedId(p.id);
+ sheetRef.current?.dismiss();
+ }}
+ onClose={() => sheetRef.current?.dismiss()}
+ />
+
+
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/index.ts b/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/index.ts
new file mode 100644
index 00000000000..37d201c4693
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/ProjectPickerSheetView/index.ts
@@ -0,0 +1,4 @@
+export {
+ ProjectPickerSheetView,
+ type ProjectPickerSheetViewProps,
+} from "./ProjectPickerSheetView";
diff --git a/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/SessionFilterSheetView.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/SessionFilterSheetView.stories.tsx
new file mode 100644
index 00000000000..7dd2553dc2f
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/SessionFilterSheetView.stories.tsx
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SessionFilterSheetView } from "./SessionFilterSheetView";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Filter sheet",
+ component: SessionFilterSheetView,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV-08 §C — full filter sheet (85vh) over dimmed sessions list. 5 workspace rows + 3 status rows multi-select + Clear all / Apply footer. Tap Apply to commit filters (useState).",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: { autoPresent: true },
+ argTypes: { autoPresent: { control: "boolean" } },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/SessionFilterSheetView.tsx b/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/SessionFilterSheetView.tsx
new file mode 100644
index 00000000000..79f2e3936b2
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/SessionFilterSheetView.tsx
@@ -0,0 +1,72 @@
+import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
+import { useEffect, useRef, useState } from "react";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
+import type { BottomSheetRef } from "@/components/BottomSheet";
+import { SessionFilterSheet } from "../../components/SessionFilterSheet";
+import { SessionsList } from "../../components/SessionsList";
+import {
+ MOCK_FILTER_WORKSPACES,
+ MOCK_PROJECT_NAME,
+ MOCK_SESSIONS,
+} from "../../mock-data";
+import type { SessionsFilters } from "../../types";
+
+export type SessionFilterSheetViewProps = {
+ className?: string;
+ autoPresent?: boolean;
+};
+
+const INITIAL: SessionsFilters = {
+ workspaceIds: ["fw1", "fw2"],
+ statuses: ["streaming"],
+};
+
+/**
+ * UC-NAV-08 §C — full filter sheet (85vh) over dimmed sessions list.
+ * Composes SessionFilterSheet organism + SessionsList behind. Apply commits
+ * the new filters and dismisses the sheet (useState).
+ */
+export function SessionFilterSheetView({
+ className,
+ autoPresent = true,
+}: SessionFilterSheetViewProps) {
+ const sheetRef = useRef(null);
+ const [filters, setFilters] = useState(INITIAL);
+
+ useEffect(() => {
+ if (autoPresent) sheetRef.current?.present();
+ }, [autoPresent]);
+
+ const filterCount = filters.workspaceIds.length + filters.statuses.length;
+
+ return (
+
+
+ {},
+ onProjectChipPress: () => {},
+ onFilterPress: () => sheetRef.current?.present(),
+ }}
+ onSessionPress={() => {}}
+ onNewChatPress={() => {}}
+ />
+ {
+ setFilters(next);
+ sheetRef.current?.dismiss();
+ }}
+ onClose={() => sheetRef.current?.dismiss()}
+ />
+
+
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/index.ts b/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/index.ts
new file mode 100644
index 00000000000..8b3f8eaa34a
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionFilterSheetView/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionFilterSheetView,
+ type SessionFilterSheetViewProps,
+} from "./SessionFilterSheetView";
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/SessionsListCombinedEmpty.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/SessionsListCombinedEmpty.stories.tsx
new file mode 100644
index 00000000000..e65d12faf4d
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/SessionsListCombinedEmpty.stories.tsx
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { ScrollView, View } from "react-native";
+import { SessionsListCombinedEmpty } from "./SessionsListCombinedEmpty";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Empty · contact sheet",
+ component: SessionsListCombinedEmpty,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "Reference contact sheet — all 5 empty variants stacked for side-by-side review. Scrollable.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const AllVariants: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/SessionsListCombinedEmpty.tsx b/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/SessionsListCombinedEmpty.tsx
new file mode 100644
index 00000000000..f1c910ad4e6
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/SessionsListCombinedEmpty.tsx
@@ -0,0 +1,60 @@
+import { View } from "react-native";
+import { Text } from "@/components/ui/text";
+import { SessionsListEmptyNoProjects } from "../SessionsListEmptyNoProjects";
+import { SessionsListEmptyNoSessions } from "../SessionsListEmptyNoSessions";
+import { SessionsListEmptyNoWorkspaces } from "../SessionsListEmptyNoWorkspaces";
+import { SessionsListFiltersNoMatch } from "../SessionsListFiltersNoMatch";
+import { SessionsListSearchNoMatch } from "../SessionsListSearchNoMatch";
+
+export type SessionsListCombinedEmptyProps = {
+ className?: string;
+};
+
+/**
+ * Reference / contact sheet — all 5 sessions-list empty states stacked
+ * vertically for design review. Each state is rendered at fractional height
+ * inside a labeled section so reviewers can compare hierarchies side-by-side.
+ */
+export function SessionsListCombinedEmpty(_: SessionsListCombinedEmptyProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+function Section({
+ label,
+ children,
+}: {
+ label: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {label}
+
+
+ {children}
+
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/index.ts b/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/index.ts
new file mode 100644
index 00000000000..607d2c85ee3
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListCombinedEmpty/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionsListCombinedEmpty,
+ type SessionsListCombinedEmptyProps,
+} from "./SessionsListCombinedEmpty";
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/SessionsListEmptyNoProjects.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/SessionsListEmptyNoProjects.stories.tsx
new file mode 100644
index 00000000000..46a41712086
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/SessionsListEmptyNoProjects.stories.tsx
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SessionsListEmptyNoProjects } from "./SessionsListEmptyNoProjects";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Empty · no projects",
+ component: SessionsListEmptyNoProjects,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV-06.1. No projects yet. Header is a plain centered 'Sessions' title; no chip/search/filter/FAB.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/SessionsListEmptyNoProjects.tsx b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/SessionsListEmptyNoProjects.tsx
new file mode 100644
index 00000000000..db31f820d21
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/SessionsListEmptyNoProjects.tsx
@@ -0,0 +1,36 @@
+import { Package } from "lucide-react-native";
+import { View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { EmptyState } from "@/components/EmptyState";
+import { Text } from "@/components/ui/text";
+import { cn } from "@/lib/utils";
+
+export type SessionsListEmptyNoProjectsProps = {
+ className?: string;
+};
+
+/**
+ * UC-NAV-06.1 — no projects yet. The header reduces to a plain centered
+ * "Sessions" title because chip/search/filter are meaningless without a
+ * project. No FAB because a workspace is required to start a chat.
+ */
+export function SessionsListEmptyNoProjects({
+ className,
+}: SessionsListEmptyNoProjectsProps) {
+ const insets = useSafeAreaInsets();
+ return (
+
+
+ Sessions
+
+
+
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/index.ts b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/index.ts
new file mode 100644
index 00000000000..e3fe715488a
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoProjects/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionsListEmptyNoProjects,
+ type SessionsListEmptyNoProjectsProps,
+} from "./SessionsListEmptyNoProjects";
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/SessionsListEmptyNoSessions.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/SessionsListEmptyNoSessions.stories.tsx
new file mode 100644
index 00000000000..d30f1fc9759
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/SessionsListEmptyNoSessions.stories.tsx
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SessionsListEmptyNoSessions } from "./SessionsListEmptyNoSessions";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Empty · no sessions",
+ component: SessionsListEmptyNoSessions,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV-06.3. Multi-project, current project has zero sessions. Multi-project chip header. FAB IS visible — only empty state where the user can create a session.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/SessionsListEmptyNoSessions.tsx b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/SessionsListEmptyNoSessions.tsx
new file mode 100644
index 00000000000..94498d1ec3d
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/SessionsListEmptyNoSessions.tsx
@@ -0,0 +1,40 @@
+import { MessageSquare } from "lucide-react-native";
+import { EmptyState } from "@/components/EmptyState";
+import { SessionsList } from "../../components/SessionsList";
+import { MOCK_PROJECT_NAME } from "../../mock-data";
+
+export type SessionsListEmptyNoSessionsProps = {
+ className?: string;
+};
+
+/**
+ * UC-NAV-06.3 — multi-project but the active project has zero sessions yet.
+ * Multi-project chip header is shown. FAB IS visible — the user can create
+ * a session from this state.
+ */
+export function SessionsListEmptyNoSessions({
+ className,
+}: SessionsListEmptyNoSessionsProps) {
+ return (
+ {},
+ onProjectChipPress: () => {},
+ onFilterPress: () => {},
+ }}
+ emptyBody={
+
+ }
+ onNewChatPress={() => {}}
+ />
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/index.ts b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/index.ts
new file mode 100644
index 00000000000..1804441595a
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoSessions/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionsListEmptyNoSessions,
+ type SessionsListEmptyNoSessionsProps,
+} from "./SessionsListEmptyNoSessions";
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/SessionsListEmptyNoWorkspaces.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/SessionsListEmptyNoWorkspaces.stories.tsx
new file mode 100644
index 00000000000..9f3974a45da
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/SessionsListEmptyNoWorkspaces.stories.tsx
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SessionsListEmptyNoWorkspaces } from "./SessionsListEmptyNoWorkspaces";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Empty · no workspaces",
+ component: SessionsListEmptyNoWorkspaces,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV-06.2. Exactly one project but zero workspaces. Header uses single-project chip variant (no chevron). No FAB.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/SessionsListEmptyNoWorkspaces.tsx b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/SessionsListEmptyNoWorkspaces.tsx
new file mode 100644
index 00000000000..f88ef07648e
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/SessionsListEmptyNoWorkspaces.tsx
@@ -0,0 +1,37 @@
+import { Layers } from "lucide-react-native";
+import { EmptyState } from "@/components/EmptyState";
+import { SessionsList } from "../../components/SessionsList";
+import { MOCK_PROJECT_NAME } from "../../mock-data";
+
+export type SessionsListEmptyNoWorkspacesProps = {
+ className?: string;
+};
+
+/**
+ * UC-NAV-06.2 — exactly one project but zero workspaces. Header uses the
+ * `single-project` chip variant (static, no chevron). No FAB.
+ */
+export function SessionsListEmptyNoWorkspaces({
+ className,
+}: SessionsListEmptyNoWorkspacesProps) {
+ return (
+ {},
+ onFilterPress: () => {},
+ }}
+ emptyBody={
+
+ }
+ />
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/index.ts b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/index.ts
new file mode 100644
index 00000000000..7027d59e88e
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListEmptyNoWorkspaces/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionsListEmptyNoWorkspaces,
+ type SessionsListEmptyNoWorkspacesProps,
+} from "./SessionsListEmptyNoWorkspaces";
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/SessionsListFiltersNoMatch.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/SessionsListFiltersNoMatch.stories.tsx
new file mode 100644
index 00000000000..5c21daf8ce2
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/SessionsListFiltersNoMatch.stories.tsx
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SessionsListFiltersNoMatch } from "./SessionsListFiltersNoMatch";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Empty · filters no-match",
+ component: SessionsListFiltersNoMatch,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV-06.5. Applied filters yield zero matches. Filter button has `·2` badge + AppliedFilterTag row shows 2 filter chips. Body: settings icon + 'No matches' + Clear filters CTA. Tap a chip's ✕ to remove individual filter (useState).",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/SessionsListFiltersNoMatch.tsx b/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/SessionsListFiltersNoMatch.tsx
new file mode 100644
index 00000000000..0caefcf00b7
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/SessionsListFiltersNoMatch.tsx
@@ -0,0 +1,69 @@
+import { Settings } from "lucide-react-native";
+import { useState } from "react";
+import { Pressable } from "react-native";
+import { EmptyState } from "@/components/EmptyState";
+import { Text } from "@/components/ui/text";
+import type { SessionsListAppliedFilter } from "../../components/SessionsList";
+import { SessionsList } from "../../components/SessionsList";
+import { MOCK_PROJECT_NAME } from "../../mock-data";
+
+export type SessionsListFiltersNoMatchProps = {
+ className?: string;
+};
+
+const INITIAL_FILTERS: ReadonlyArray = [
+ { id: "f1", kind: "workspace", label: "main · desktop" },
+ { id: "f2", kind: "status", label: "Streaming" },
+];
+
+/**
+ * UC-NAV-06.5 — applied filters yield zero matches. Multi-project chip with
+ * `·N` badge on filter button + AppliedFilterTag row below header showing
+ * the 2 filters. Body: settings icon + "No matches" + Clear filters CTA.
+ * No FAB.
+ */
+export function SessionsListFiltersNoMatch({
+ className,
+}: SessionsListFiltersNoMatchProps) {
+ const [filters, setFilters] =
+ useState>(INITIAL_FILTERS);
+
+ const clearAll = () => setFilters([]);
+
+ return (
+
+ setFilters((curr) => curr.filter((f) => f.id !== id))
+ }
+ onClearFilters={clearAll}
+ headerProps={{
+ variant: "multi-project",
+ filterCount: filters.length,
+ onMenuPress: () => {},
+ onProjectChipPress: () => {},
+ onFilterPress: () => {},
+ }}
+ emptyBody={
+
+ Clear filters
+
+ }
+ />
+ }
+ />
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/index.ts b/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/index.ts
new file mode 100644
index 00000000000..dc4c81672b1
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListFiltersNoMatch/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionsListFiltersNoMatch,
+ type SessionsListFiltersNoMatchProps,
+} from "./SessionsListFiltersNoMatch";
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListLoaded/SessionsListLoaded.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionsListLoaded/SessionsListLoaded.stories.tsx
new file mode 100644
index 00000000000..ab04e755f56
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListLoaded/SessionsListLoaded.stories.tsx
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SessionsListLoaded } from "./SessionsListLoaded";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Loaded (canonical)",
+ component: SessionsListLoaded,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV §A canonical. Project-first chrome: ProjectChipHeader (multi-project) with hamburger + project chip + search + filter. FlatList of 5 SessionRow showing the full status spectrum. NewChatFab anchored bottom-right.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: { filterCount: 0 },
+ argTypes: {
+ filterCount: { control: { type: "number", min: 0, max: 9 } },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithFilterBadge: Story = {
+ args: {
+ filterCount: 2,
+ appliedFilters: [
+ { id: "f1", kind: "workspace", label: "main · desktop" },
+ { id: "f2", kind: "status", label: "Streaming" },
+ ],
+ },
+};
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListLoaded/SessionsListLoaded.tsx b/apps/mobile/screens/sessions-list/views/SessionsListLoaded/SessionsListLoaded.tsx
new file mode 100644
index 00000000000..a93080b5a22
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListLoaded/SessionsListLoaded.tsx
@@ -0,0 +1,45 @@
+import { useState } from "react";
+import type { SessionsListAppliedFilter } from "../../components/SessionsList";
+import { SessionsList } from "../../components/SessionsList";
+import { MOCK_PROJECT_NAME, MOCK_SESSIONS } from "../../mock-data";
+
+export type SessionsListLoadedProps = {
+ className?: string;
+ filterCount?: number;
+ appliedFilters?: ReadonlyArray;
+};
+
+/**
+ * UC-NAV §A — canonical loaded sessions-list with project-first chrome.
+ * 5 session rows showing the full status spectrum (live × 2, idle, warning,
+ * archived). Configurable filter count + applied-filter row via props.
+ */
+export function SessionsListLoaded({
+ className,
+ filterCount = 0,
+ appliedFilters,
+}: SessionsListLoadedProps) {
+ const [search, setSearch] = useState("");
+
+ return (
+ setSearch(""),
+ filterCount,
+ onMenuPress: () => {},
+ onProjectChipPress: () => {},
+ onFilterPress: () => {},
+ }}
+ onSessionPress={() => {}}
+ onSessionLongPress={() => {}}
+ onNewChatPress={() => {}}
+ />
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListLoaded/index.ts b/apps/mobile/screens/sessions-list/views/SessionsListLoaded/index.ts
new file mode 100644
index 00000000000..20d778c41ff
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListLoaded/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionsListLoaded,
+ type SessionsListLoadedProps,
+} from "./SessionsListLoaded";
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/SessionsListSearchNoMatch.stories.tsx b/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/SessionsListSearchNoMatch.stories.tsx
new file mode 100644
index 00000000000..7468ecbf0bc
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/SessionsListSearchNoMatch.stories.tsx
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from "@storybook/react-native";
+import { View } from "react-native";
+import { SessionsListSearchNoMatch } from "./SessionsListSearchNoMatch";
+
+const meta: Meta = {
+ title: "Views/Sessions/01-SessionsList · Empty · search no-match",
+ component: SessionsListSearchNoMatch,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ "UC-NAV-06.4. Active search query yields zero matches. Multi-project chip + populated search input + visible clear button. Body: search icon + 'No matches' + Clear search CTA. No FAB.",
+ },
+ },
+ layout: "fullscreen",
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ args: { initialQuery: "zzzz" },
+ argTypes: { initialQuery: { control: "text" } },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/SessionsListSearchNoMatch.tsx b/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/SessionsListSearchNoMatch.tsx
new file mode 100644
index 00000000000..5087529bcf2
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/SessionsListSearchNoMatch.tsx
@@ -0,0 +1,58 @@
+import { Search } from "lucide-react-native";
+import { useState } from "react";
+import { Pressable } from "react-native";
+import { EmptyState } from "@/components/EmptyState";
+import { Text } from "@/components/ui/text";
+import { SessionsList } from "../../components/SessionsList";
+import { MOCK_PROJECT_NAME } from "../../mock-data";
+
+export type SessionsListSearchNoMatchProps = {
+ className?: string;
+ initialQuery?: string;
+};
+
+/**
+ * UC-NAV-06.4 — search query returns zero matches in the current project.
+ * Multi-project chip header with the populated search input + visible clear
+ * button. Body: oversized search icon + "No matches" + Clear search CTA.
+ * No FAB.
+ */
+export function SessionsListSearchNoMatch({
+ className,
+ initialQuery = "zzzz",
+}: SessionsListSearchNoMatchProps) {
+ const [query, setQuery] = useState(initialQuery);
+ return (
+ setQuery(""),
+ onMenuPress: () => {},
+ onProjectChipPress: () => {},
+ onFilterPress: () => {},
+ }}
+ emptyBody={
+ setQuery("")}
+ >
+ Clear search
+
+ }
+ />
+ }
+ />
+ );
+}
diff --git a/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/index.ts b/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/index.ts
new file mode 100644
index 00000000000..539826864b0
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/SessionsListSearchNoMatch/index.ts
@@ -0,0 +1,4 @@
+export {
+ SessionsListSearchNoMatch,
+ type SessionsListSearchNoMatchProps,
+} from "./SessionsListSearchNoMatch";
diff --git a/apps/mobile/screens/sessions-list/views/index.ts b/apps/mobile/screens/sessions-list/views/index.ts
new file mode 100644
index 00000000000..e9c63e0722f
--- /dev/null
+++ b/apps/mobile/screens/sessions-list/views/index.ts
@@ -0,0 +1,10 @@
+export * from "./NewChatSheetView";
+export * from "./ProjectPickerSheetView";
+export * from "./SessionFilterSheetView";
+export * from "./SessionsListCombinedEmpty";
+export * from "./SessionsListEmptyNoProjects";
+export * from "./SessionsListEmptyNoSessions";
+export * from "./SessionsListEmptyNoWorkspaces";
+export * from "./SessionsListFiltersNoMatch";
+export * from "./SessionsListLoaded";
+export * from "./SessionsListSearchNoMatch";
diff --git a/bun.lock b/bun.lock
index b13c71e1135..999477f733c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -111,7 +111,7 @@
},
"apps/desktop": {
"name": "@superset/desktop",
- "version": "1.10.3",
+ "version": "1.11.1",
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.36",
@@ -463,8 +463,11 @@
"dependencies": {
"@better-auth/expo": "1.6.5",
"@electric-sql/client": "1.5.15",
+ "@expo-google-fonts/geist": "0.4.2",
+ "@expo-google-fonts/geist-mono": "0.4.2",
"@expo/ui": "56.0.12",
"@expo/vector-icons": "15.1.1",
+ "@gorhom/bottom-sheet": "5.2.12",
"@react-native-async-storage/async-storage": "2.2.0",
"@rn-primitives/accordion": "1.4.0",
"@rn-primitives/alert-dialog": "1.4.0",
@@ -510,13 +513,16 @@
"expo-dev-client": "56.0.14",
"expo-device": "56.0.4",
"expo-file-system": "56.0.7",
+ "expo-font": "56.0.0",
"expo-glass-effect": "56.0.4",
"expo-image": "56.0.8",
+ "expo-linear-gradient": "56.0.4",
"expo-linking": "56.0.11",
"expo-localization": "56.0.6",
"expo-network": "56.0.4",
"expo-router": "56.2.5",
"expo-secure-store": "56.0.4",
+ "expo-splash-screen": "56.0.0",
"expo-status-bar": "56.0.4",
"expo-system-ui": "56.0.5",
"expo-web-browser": "56.0.5",
@@ -539,10 +545,14 @@
"zod": "4.3.6",
},
"devDependencies": {
+ "@storybook/addon-ondevice-actions": "^9",
+ "@storybook/addon-ondevice-controls": "^9",
+ "@storybook/react-native": "^9",
"@types/node": "24.12.0",
"@types/react": "19.2.14",
"babel-preset-expo": "56.0.11",
"expo-mcp": "0.2.4",
+ "storybook": "^9",
"typescript": "5.9.3",
},
},
@@ -769,7 +779,7 @@
},
"packages/host-service": {
"name": "@superset/host-service",
- "version": "0.8.11",
+ "version": "0.8.13",
"dependencies": {
"@hono/node-server": "1.19.13",
"@hono/node-ws": "1.3.0",
@@ -1607,6 +1617,10 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+ "@expo-google-fonts/geist": ["@expo-google-fonts/geist@0.4.2", "", {}, "sha512-cTsqJRQrvK5EniTVwH7UDK18RFf9Bc6yxciAuM936aViQ+eR6j3mLEI2rQkJXz5JP9kwtn8tHJviDyZ0ulH6oA=="],
+
+ "@expo-google-fonts/geist-mono": ["@expo-google-fonts/geist-mono@0.4.2", "", {}, "sha512-Ei2dBHXBgCL0pNi1RSuWIroOsvaNBeyT+2oE/q9vQBzZyqWuKYZ5yrybTeIeDl7MEWTgxBVUjyrws3JVbVac8Q=="],
+
"@expo-google-fonts/material-symbols": ["@expo-google-fonts/material-symbols@0.4.27", "", {}, "sha512-cnb3DZnWUWpezGFkJ8y4MT5f/lw6FcgDzeJzic+T+vpQHLHG1cg3SC3i1w1i8Bk4xKR4HPY3t9iIRNvtr5ml8A=="],
"@expo/cli": ["@expo/cli@56.1.10", "", { "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~56.0.8", "@expo/config-plugins": "~56.0.7", "@expo/devcert": "^1.2.1", "@expo/env": "~2.3.0", "@expo/image-utils": "^0.10.0", "@expo/inline-modules": "^0.0.9", "@expo/json-file": "^10.2.0", "@expo/log-box": "^56.0.12", "@expo/metro": "~56.0.0", "@expo/metro-config": "~56.0.11", "@expo/metro-file-map": "^56.0.3", "@expo/osascript": "^2.6.0", "@expo/package-manager": "^1.12.0", "@expo/plist": "^0.7.0", "@expo/prebuild-config": "^56.0.12", "@expo/require-utils": "^56.1.2", "@expo/router-server": "^56.0.11", "@expo/schema-utils": "^56.0.0", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.4.4", "@react-native/dev-middleware": "0.85.3", "accepts": "^1.3.8", "arg": "^5.0.2", "bplist-creator": "0.1.0", "bplist-parser": "^0.3.1", "chalk": "^4.0.0", "ci-info": "^3.3.0", "compression": "^1.7.4", "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.4", "expo-server": "^56.0.4", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", "multitars": "^1.0.0", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", "picomatch": "^4.0.4", "pretty-format": "^29.7.0", "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", "stacktrace-parser": "^0.1.10", "structured-headers": "^0.4.1", "terminal-link": "^2.1.1", "toqr": "^0.1.1", "wrap-ansi": "^7.0.0", "ws": "^8.12.1", "zod": "^3.25.76" }, "peerDependencies": { "expo": "*", "expo-router": "*", "react-native": "*" }, "optionalPeers": ["expo-router", "react-native"], "bin": { "expo-internal": "main.js" } }, "sha512-OOmtxvNHfH1W+dbOlY6WcjQBdf3crmn8XCDy1/yZ7bIrXJq/HxBPxxpn6Gh7gpRby1ov7uZueN0XROiVq3tVxw=="],
@@ -1631,7 +1645,7 @@
"@expo/fingerprint": ["@expo/fingerprint@0.19.1", "", { "dependencies": { "@expo/env": "^2.3.0", "@expo/spawn-async": "^1.8.0", "arg": "^5.0.2", "chalk": "^4.1.2", "debug": "^4.3.4", "getenv": "^2.0.0", "glob": "^13.0.0", "ignore": "^5.3.1", "minimatch": "^10.2.2", "resolve-from": "^5.0.0", "semver": "^7.6.0" }, "bin": { "fingerprint": "bin/cli.js" } }, "sha512-dD6wcETUQrcmPeYvmZDFRWiyRKnCZv4HwLjVTGE45yLEYLnsci1W9NydYtFyZKdT3sS5rmgYl2yPCk2iiBdhjw=="],
- "@expo/image-utils": ["@expo/image-utils@0.8.12", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A=="],
+ "@expo/image-utils": ["@expo/image-utils@0.9.0", "", { "dependencies": { "@expo/require-utils": "56.0.0", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "semver": "^7.6.0" } }, "sha512-cDOBFjVYmo9EqpDp3weFKp9TOdn0C6xE4ON5DNrT80Tl3OI1OiTSTh/XdCvo8IVv7rtCUqkCzLWSynBa0jCSQw=="],
"@expo/inline-modules": ["@expo/inline-modules@0.0.9", "", { "dependencies": { "@expo/config-plugins": "~56.0.7" } }, "sha512-otMUXI4mOjytbe9OQ3i5X4SV0LP1GpzqLdr9+rdxUc1b0FjdvbTM/GkcbrwY4pU0fGSK0qFqX+jgSieyi+XbUA=="],
@@ -1697,6 +1711,10 @@
"@google/genai": ["@google/genai@1.49.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hO69Zl0H3x+L0KL4stl1pLYgnqnwHoLqtKy6MRlNnW8TAxjqMdOUVafomKd4z1BePkzoxJWbYILny9a2Zk43VQ=="],
+ "@gorhom/bottom-sheet": ["@gorhom/bottom-sheet@5.2.12", "", { "dependencies": { "@gorhom/portal": "1.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-native": "*", "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.16.1", "react-native-reanimated": ">=3.16.0 || >=4.0.0-" }, "optionalPeers": ["@types/react", "@types/react-native"] }, "sha512-4/SUU8cyFBU2WCLfyqkk/Tveo2XFeaPf8lUmmQ1cG9qVnBSP2+m8Gmk0jpoef2qOm+Ow3Dxo/ii1eZ4arQmgiw=="],
+
+ "@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="],
+
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
"@headless-tree/core": ["@headless-tree/core@1.6.3", "", {}, "sha512-en0EOaZfiCRF2B8DEnhGhSaUf3hVr9Bauye8G8aswPbHOKSyhJiN4bsczz1GvqF4Xb7Ga3LP0vLA6Zih7YLoyw=="],
@@ -2329,6 +2347,10 @@
"@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="],
+ "@react-native-community/datetimepicker": ["@react-native-community/datetimepicker@9.1.0", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": ">=52.0.0", "react": "*", "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["expo", "react-native-windows"] }, "sha512-eadbnk+I2vxvW30iTAsm/qlCnMMAadkifIMYNEB2lzhxN/SvlKc7S2V4k5DyrwjdCbqdcMk3t9K6fnUMcAV34w=="],
+
+ "@react-native-community/slider": ["@react-native-community/slider@5.2.0", "", {}, "sha512-484sH8aWEaSjxaZ7HT3YZ8CKDcNes2synko1vdEz5DFEdvKAduxKJTj22L/qBMD7rtIkfbX69DMzWDAGbOAV6w=="],
+
"@react-native-masked-view/masked-view": ["@react-native-masked-view/masked-view@0.3.2", "", { "peerDependencies": { "react": ">=16", "react-native": ">=0.57" } }, "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ=="],
"@react-native/assets-registry": ["@react-native/assets-registry@0.85.3", "", {}, "sha512-u9ZiYP23vA2IFtdFQFmetzSmk6SM0xgKIoiOsr1hXNHjHaLhOm+/Ph1ud57wX6+Dbwdzx8coJgnzSKL3W21PCg=="],
@@ -2601,6 +2623,24 @@
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
+ "@storybook/addon-ondevice-actions": ["@storybook/addon-ondevice-actions@9.1.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "fast-deep-equal": "^2.0.1" }, "peerDependencies": { "react": "*", "react-native": "*", "storybook": ">=9" } }, "sha512-W68LZu/M+7LpFmAuqkrYAW6UyfczvucbH6b0jNORz8jno63qLO1R8hsTLJnnz9FFLWoAR6njuUX4gPgSy1Xtkw=="],
+
+ "@storybook/addon-ondevice-controls": ["@storybook/addon-ondevice-controls@9.1.4", "", { "dependencies": { "@storybook/react-native-theming": "^9.1.4", "@storybook/react-native-ui-common": "^9.1.4", "deep-equal": "^1.0.1", "prop-types": "^15.7.2", "react-native-modal-datetime-picker": "^18.0.0", "tinycolor2": "^1.4.1" }, "peerDependencies": { "@gorhom/bottom-sheet": ">=4", "@react-native-community/datetimepicker": "*", "@react-native-community/slider": "*", "react": "*", "react-native": "*", "storybook": ">=9" }, "optionalPeers": ["@gorhom/bottom-sheet"] }, "sha512-oj+Ga41RSYMUOwgrMPaLG5fTI2puGmrTuxHGNN+Up1Lxqs5mr2fzWDO10TEspZtAg/ETVZqtNKhW166wQoCBTA=="],
+
+ "@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="],
+
+ "@storybook/react": ["@storybook/react@9.1.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "9.1.20" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.1.20", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-TJhqzggs7HCvLhTXKfx8HodnVq9YizsB2J31s9v6olU0UCxbCY+FYaCF+XdE8qUCyefGRZgHKzGBIczJ/q9e2g=="],
+
+ "@storybook/react-dom-shim": ["@storybook/react-dom-shim@9.1.20", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.1.20" } }, "sha512-UYdZavfPwHEqCKMqPssUOlyFVZiJExLxnSHwkICSZBmw3gxXJcp1aXWs7PvoZdWz2K4ztl3IcKErXXHeiY6w+A=="],
+
+ "@storybook/react-native": ["@storybook/react-native@9.1.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react": "^9.1.8", "@storybook/react-native-theming": "^9.1.4", "@storybook/react-native-ui": "^9.1.4", "@storybook/react-native-ui-common": "^9.1.4", "commander": "^8.2.0", "dedent": "^1.5.1", "deepmerge": "^4.3.0", "react-native-url-polyfill": "^2.0.0", "setimmediate": "^1.0.5", "type-fest": "~2.19", "util": "^0.12.4", "ws": "^8.18.0" }, "peerDependencies": { "@gorhom/bottom-sheet": ">=4", "react": "*", "react-native": ">=0.72.0", "react-native-gesture-handler": ">=2", "react-native-reanimated": ">=2", "react-native-safe-area-context": "*", "storybook": ">=9" }, "optionalPeers": ["@gorhom/bottom-sheet", "react-native-gesture-handler", "react-native-reanimated", "react-native-safe-area-context"], "bin": { "sb-rn-get-stories": "./bin/get-stories.js" } }, "sha512-OjR/1rPQZ2SMBU4+tlgAsF2ooPI1rVadf8DfCH3kC0K4dSfEI+ocqTVzv7p4N3W2ZyTaxgJKJoAxA/Ohw+cSbA=="],
+
+ "@storybook/react-native-theming": ["@storybook/react-native-theming@9.1.4", "", { "dependencies": { "polished": "^4.3.1" }, "peerDependencies": { "react": "*", "react-native": ">=0.57.0" } }, "sha512-gUo9ziv5YIYlgU5b9BpkUcMHyV4uJ/gmb8LWZpROx150HBavmX2IVd8IwBiJkPNuWrTFlTMPW9ytU740rp+GPQ=="],
+
+ "@storybook/react-native-ui": ["@storybook/react-native-ui@9.1.4", "", { "dependencies": { "@storybook/react": "^9.1.8", "@storybook/react-native-theming": "^9.1.4", "@storybook/react-native-ui-common": "^9.1.4", "es-toolkit": "^1.38.0", "fuse.js": "^7.0.0", "memoizerific": "^1.11.3", "polished": "^4.3.1", "store2": "^2.14.3" }, "peerDependencies": { "@gorhom/bottom-sheet": ">=4", "react": "*", "react-native": ">=0.57.0", "react-native-gesture-handler": ">=2", "react-native-reanimated": ">=3", "react-native-safe-area-context": "*", "react-native-svg": ">=14", "storybook": ">=9" } }, "sha512-fBCDfSLkEqgXpAzVdmJMdAxMCkgAhDVVvxvJrtegjOFLE8o9d3PxfFjJkVbZntKmWVU7XlVn0NFuzC0e+5hHIQ=="],
+
+ "@storybook/react-native-ui-common": ["@storybook/react-native-ui-common@9.1.4", "", { "dependencies": { "@storybook/react": "^9.1.8", "@storybook/react-native-theming": "^9.1.4", "es-toolkit": "^1.38.0", "fuse.js": "^7.0.0", "memoizerific": "^1.11.3", "polished": "^4.3.1", "store2": "^2.14.3", "ts-dedent": "^2.2.0" }, "peerDependencies": { "react": "*", "react-native": ">=0.57.0", "storybook": ">=9" } }, "sha512-BfLxoGrVoWZkaeZrqf1cXjnXVZup1knguGNBHFKtWRQLAZqmxEGEYM6l1hzODXvhnsD+2eX+KHQ5DU5CnMiPiw=="],
+
"@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="],
"@superset/admin": ["@superset/admin@workspace:apps/admin"],
@@ -2903,6 +2943,8 @@
"@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
@@ -2973,6 +3015,8 @@
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
"@types/draco3d": ["@types/draco3d@1.4.10", "", {}, "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw=="],
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
@@ -3143,6 +3187,16 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
+ "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
+
+ "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
+
+ "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
+
+ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
+
"@vue/compiler-core": ["@vue/compiler-core@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.31", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="],
@@ -3303,6 +3357,8 @@
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
@@ -3331,6 +3387,8 @@
"ava": ["ava@5.3.1", "", { "dependencies": { "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "ansi-styles": "^6.2.1", "arrgv": "^1.0.2", "arrify": "^3.0.0", "callsites": "^4.0.0", "cbor": "^8.1.0", "chalk": "^5.2.0", "chokidar": "^3.5.3", "chunkd": "^2.0.1", "ci-info": "^3.8.0", "ci-parallel-vars": "^1.0.1", "clean-yaml-object": "^0.1.0", "cli-truncate": "^3.1.0", "code-excerpt": "^4.0.0", "common-path-prefix": "^3.0.0", "concordance": "^5.0.4", "currently-unhandled": "^0.4.1", "debug": "^4.3.4", "emittery": "^1.0.1", "figures": "^5.0.0", "globby": "^13.1.4", "ignore-by-default": "^2.1.0", "indent-string": "^5.0.0", "is-error": "^2.2.2", "is-plain-object": "^5.0.0", "is-promise": "^4.0.0", "matcher": "^5.0.0", "mem": "^9.0.2", "ms": "^2.1.3", "p-event": "^5.0.1", "p-map": "^5.5.0", "picomatch": "^2.3.1", "pkg-conf": "^4.0.0", "plur": "^5.1.0", "pretty-ms": "^8.0.0", "resolve-cwd": "^3.0.0", "stack-utils": "^2.0.6", "strip-ansi": "^7.0.1", "supertap": "^3.0.1", "temp-dir": "^3.0.0", "write-file-atomic": "^5.0.1", "yargs": "^17.7.2" }, "peerDependencies": { "@ava/typescript": "*" }, "optionalPeers": ["@ava/typescript"], "bin": { "ava": "entrypoints/cli.mjs" } }, "sha512-Scv9a4gMOXB6+ni4toLuhAm9KYWEjsgBglJl+kMGI5+IVDt120CCDZyB5HNU9DjmLI2t4I0GbnxGLmmRfGTJGg=="],
+ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
+
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
@@ -3387,6 +3445,8 @@
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
+ "better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="],
+
"better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
@@ -3451,6 +3511,8 @@
"cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
+ "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
+
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
@@ -3467,6 +3529,8 @@
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+ "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
+
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
@@ -3479,6 +3543,8 @@
"chat": ["chat@4.24.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" } }, "sha512-0TxglwtGRMGlqERuHVZZ27Z4YBeZH3oRXCqHZYuI41L7xcSHF5C3wEHTMdVqHp3p8ZKQcKYQPOwYWvaeFVa4+g=="],
+ "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
+
"cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="],
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
@@ -3765,6 +3831,12 @@
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
+ "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="],
+
+ "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
+
+ "deep-equal": ["deep-equal@1.1.2", "", { "dependencies": { "is-arguments": "^1.1.1", "is-date-object": "^1.0.5", "is-regex": "^1.1.4", "object-is": "^1.1.5", "object-keys": "^1.1.1", "regexp.prototype.flags": "^1.5.1" } }, "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg=="],
+
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"deep-rename-keys": ["deep-rename-keys@0.2.1", "", { "dependencies": { "kind-of": "^3.0.2", "rename-keys": "^1.1.2" } }, "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A=="],
@@ -3781,6 +3853,8 @@
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
+ "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
+
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
@@ -4031,7 +4105,7 @@
"expo-file-system": ["expo-file-system@56.0.7", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA=="],
- "expo-font": ["expo-font@55.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-ZKeGTFffPygvY5dM/9ATM2p7QDkhsaHopH7wFAWgP2lKzqUMS9B/RxCvw5CaObr9Ro7x9YptyeRKX2HmgmMfrg=="],
+ "expo-font": ["expo-font@56.0.0", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "56.0.0-preview.0", "react": "*", "react-native": "*" } }, "sha512-zav71114PjD4n0NS5AfE6U0WbkO3VDHxVDelZJ8cENG0saF+VniNKZisdPcnhdQpQXsg7rx1Q8UGS6uyIMlSwg=="],
"expo-glass-effect": ["expo-glass-effect@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-xI9rXtDwi7RW82uAlfyaXO6+k21ApWJ2tHAWYqPr/FjfmZbKsgNJ4Q0iZzGPCwboqjTGxaRZ61SZxBl8hDt5iA=="],
@@ -4041,6 +4115,8 @@
"expo-keep-awake": ["expo-keep-awake@56.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CLMJXtEiMKknD3Rpm8CRwE6ZJUzu2yCEmRk1sgfHAJ1zIbuEWY3dpPDubtsnuzWm+2k6Sru+yaFbYsvPWmTiBA=="],
+ "expo-linear-gradient": ["expo-linear-gradient@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-KUp1dNSRtuMyiExhf6FJf5YUtmw2cRaPytl10HQi7isj5Yac38udmD55T2tglNYTZlvgT5+oflpyFoH15hmOcw=="],
+
"expo-linking": ["expo-linking@56.0.11", "", { "dependencies": { "expo-constants": "~56.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MEPgML2mqm2Y8rP6zTleOpCmYiFyfQfNSOBpDIb7CYpbDQleStugvceKsEsL4v8C0Dl5u7e8KkkrbqmgpOOIBw=="],
"expo-localization": ["expo-localization@56.0.6", "", { "dependencies": { "rtl-detect": "^1.0.2" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-zzBVoUFHCVNBywcxGsspoZeIXebihOo/AnmQYE4jMv8gHCSKlLNFT+ft+0+mWcZCMs9necvUs8S8TDonAu/xBA=="],
@@ -4063,6 +4139,8 @@
"expo-server": ["expo-server@56.0.4", "", {}, "sha512-4dJ57KuAwDl7eQGD6aG9kTzBIftWAfHH1+6Zxy7NcPCBrKYis3/H5enGUz1asH8HHhONXfJ5BdJqfEWAEAgWxA=="],
+ "expo-splash-screen": ["expo-splash-screen@56.0.0", "", { "dependencies": { "@expo/config-plugins": "56.0.0", "@expo/image-utils": "0.9.0", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "56.0.0-preview.0" } }, "sha512-Mue5mdczeanNwUlJq9A4joYGIyAh42S1z2J/Unfhm9hkYSC/l56QdUld8gdyPAkFstNJaZacb2mXAGuFC+jerQ=="],
+
"expo-status-bar": ["expo-status-bar@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-IGs/fDfkHXofy2ZQrGiXayhFK04HB85FZXorhcEhDZEcqASKgSqpak+HwUtAaR0MeTJwWyHNF7I6VmVbbp8EcA=="],
"expo-symbols": ["expo-symbols@56.0.5", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-RIukH0Xo80C7RU8qreipL2SPy2Py+Km8JFPbCmbPQpHkM3DW9Znlmg6VfhzbtUOlO5EuNSF0lAJ3l2VJi6qYrw=="],
@@ -4159,6 +4237,8 @@
"fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="],
+ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
+
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
@@ -4201,6 +4281,8 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
+
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
"gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
@@ -4211,6 +4293,8 @@
"geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="],
+ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
+
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
@@ -4415,14 +4499,20 @@
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+ "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
+
"is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="],
+ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
+
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
+
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
@@ -4439,6 +4529,8 @@
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
+ "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
@@ -4461,10 +4553,14 @@
"is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
+ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
+
"is-standalone-pwa": ["is-standalone-pwa@0.1.1", "", {}, "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
+ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
+
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
@@ -4657,6 +4753,8 @@
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
+
"lowdb": ["lowdb@7.0.1", "", { "dependencies": { "steno": "^4.0.2" } }, "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw=="],
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
@@ -4683,6 +4781,8 @@
"map-age-cleaner": ["map-age-cleaner@0.1.3", "", { "dependencies": { "p-defer": "^1.0.0" } }, "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w=="],
+ "map-or-similar": ["map-or-similar@1.5.0", "", {}, "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg=="],
+
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
@@ -4747,6 +4847,8 @@
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
+ "memoizerific": ["memoizerific@1.11.3", "", { "dependencies": { "map-or-similar": "^1.5.0" } }, "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog=="],
+
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="],
@@ -5003,6 +5105,8 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+ "object-is": ["object-is@1.1.6", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
+
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"ollama-ai-provider-v2": ["ollama-ai-provider-v2@1.5.5", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.17" }, "peerDependencies": { "zod": "^4.0.16" } }, "sha512-1YwTFdPjhPNHny/DrOHO+s8oVGGIE5Jib61/KnnjPRNWQhVVimrJJdaAX3e6nNRRDXrY5zbb9cfm2+yVvgsrqw=="],
@@ -5025,7 +5129,7 @@
"onnxruntime-node": ["onnxruntime-node@1.21.0", "", { "dependencies": { "global-agent": "^3.0.0", "onnxruntime-common": "1.21.0", "tar": "^7.0.1" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw=="],
- "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
+ "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
@@ -5113,6 +5217,8 @@
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
+
"pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="],
"peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="],
@@ -5171,8 +5277,12 @@
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
+ "polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="],
+
"portfinder": ["portfinder@1.0.38", "", { "dependencies": { "async": "^3.2.6", "debug": "^4.3.6" } }, "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg=="],
+ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
+
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
@@ -5365,6 +5475,8 @@
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="],
+ "react-native-modal-datetime-picker": ["react-native-modal-datetime-picker@18.0.0", "", { "dependencies": { "prop-types": "^15.7.2" }, "peerDependencies": { "@react-native-community/datetimepicker": ">=6.7.0", "react-native": ">=0.65.0" } }, "sha512-0jdvhhraZQlRACwr7pM6vmZ2kxgzJ4CpnmV6J3TVA6MrXMXK6Zo/upRBKkRp0+fTOiKuNblzesA2U59rYo6SGA=="],
+
"react-native-reanimated": ["react-native-reanimated@4.3.1", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.81 - 0.85", "react-native-worklets": "0.8.x" } }, "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q=="],
"react-native-safe-area-context": ["react-native-safe-area-context@5.8.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-t+ZsAVzY/wWzzx34vqGbo3/as9EEESJdbyZNL7Yg5EYX+toYMtMqFoDDCvqZUi35eeGVsXc6pAaEk4edMwbuCQ=="],
@@ -5373,6 +5485,8 @@
"react-native-svg": ["react-native-svg@15.15.5", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w=="],
+ "react-native-url-polyfill": ["react-native-url-polyfill@2.0.0", "", { "dependencies": { "whatwg-url-without-unicode": "8.0.0-3" }, "peerDependencies": { "react-native": "*" } }, "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA=="],
+
"react-native-worklets": ["react-native-worklets@0.8.3", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-classes": "^7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "convert-source-map": "^2.0.0", "semver": "^7.7.3" }, "peerDependencies": { "@babel/core": "*", "@react-native/metro-config": "*", "react": "*", "react-native": "0.81 - 0.85" } }, "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg=="],
"react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="],
@@ -5439,6 +5553,8 @@
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
+ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
+
"regexpu-core": ["regexpu-core@6.4.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" } }, "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA=="],
"regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="],
@@ -5559,6 +5675,8 @@
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
+
"safe-regex2": ["safe-regex2@5.1.0", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
@@ -5601,6 +5719,10 @@
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
+ "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
+
+ "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
+
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
@@ -5731,6 +5853,10 @@
"steno": ["steno@4.0.2", "", {}, "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A=="],
+ "store2": ["store2@2.14.4", "", {}, "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw=="],
+
+ "storybook": ["storybook@9.1.20", "", { "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/spy": "3.2.4", "better-opn": "^3.0.2", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", "esbuild-register": "^3.5.0", "recast": "^0.23.5", "semver": "^7.6.2", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./bin/index.cjs" }, "sha512-6rME2tww6PFhm96iG2Xx44yzwLDWBiDWy+kJ2ub6x90werSTOiuo+tZJ94BgCfFutR0tEfLRIq59s+Zg6YyChA=="],
+
"stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="],
"stream-parser": ["stream-parser@0.3.1", "", { "dependencies": { "debug": "2" } }, "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ=="],
@@ -5869,10 +5995,16 @@
"tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="],
+ "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
+
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+ "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
+
+ "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
+
"tippy.js": ["tippy.js@6.3.7", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="],
"tiptap-markdown": ["tiptap-markdown@0.9.0", "", { "dependencies": { "@types/markdown-it": "^13.0.7", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "prosemirror-markdown": "^1.11.1" }, "peerDependencies": { "@tiptap/core": "^3.0.1" } }, "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ=="],
@@ -6035,6 +6167,8 @@
"utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
+ "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
+
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
@@ -6105,7 +6239,7 @@
"webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="],
- "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+ "webidl-conversions": ["webidl-conversions@5.0.0", "", {}, "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="],
"webpack": ["webpack@5.105.4", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw=="],
@@ -6125,10 +6259,14 @@
"whatwg-url-minimum": ["whatwg-url-minimum@0.1.2", "", {}, "sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A=="],
+ "whatwg-url-without-unicode": ["whatwg-url-without-unicode@8.0.0-3", "", { "dependencies": { "buffer": "^5.4.3", "punycode": "^2.1.1", "webidl-conversions": "^5.0.0" } }, "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig=="],
+
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
+
"widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
"workerd": ["workerd@1.20260317.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260317.1", "@cloudflare/workerd-darwin-arm64": "1.20260317.1", "@cloudflare/workerd-linux-64": "1.20260317.1", "@cloudflare/workerd-linux-arm64": "1.20260317.1", "@cloudflare/workerd-windows-64": "1.20260317.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g=="],
@@ -6381,7 +6519,7 @@
"@expo/fingerprint/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
- "@expo/image-utils/@expo/spawn-async": ["@expo/spawn-async@1.7.2", "", { "dependencies": { "cross-spawn": "^7.0.3" } }, "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew=="],
+ "@expo/image-utils/@expo/require-utils": ["@expo/require-utils@56.0.0", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "@babel/core": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8" }, "peerDependencies": { "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-btaotBF3yymrv9c7edNqPBFAo9npSPB1LMvKa0dZWEDxYZQHGAA9xnQfmdImZDe+BIgxEjzpDjDq1T6ZMi43kQ=="],
"@expo/image-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -6417,6 +6555,8 @@
"@fumadocs/ui/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
+ "@gorhom/portal/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
+
"@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@langchain/core/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
@@ -6531,6 +6671,8 @@
"@react-native/dev-middleware/chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="],
+ "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
+
"@react-native/dev-middleware/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
"@react-native/dev-middleware/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
@@ -6595,6 +6737,12 @@
"@slack/web-api/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
+ "@storybook/addon-ondevice-actions/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
+
+ "@storybook/react-native/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+
+ "@storybook/react-native/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
+
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
@@ -6633,6 +6781,8 @@
"@upstash/qstash/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
+ "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
"@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@wdio/config/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
@@ -6823,6 +6973,8 @@
"expo/react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
+ "expo-asset/@expo/image-utils": ["@expo/image-utils@0.8.12", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "resolve-from": "^5.0.0", "semver": "^7.6.0" } }, "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A=="],
+
"expo-asset/expo-constants": ["expo-constants@55.0.9", "", { "dependencies": { "@expo/config": "~55.0.10", "@expo/env": "~2.1.1" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-iBiXjZeuU5S/8docQeNzsVvtDy4w0zlmXBpFEi1ypwugceEpdQQab65TVRbusXAcwpNVxCPMpNlDssYp0Pli2g=="],
"expo-device/ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="],
@@ -6839,6 +6991,10 @@
"expo-router/react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
+ "expo-splash-screen/@expo/config-plugins": ["@expo/config-plugins@56.0.0", "", { "dependencies": { "@expo/config-types": "56.0.0", "@expo/json-file": "10.1.0", "@expo/plist": "0.6.0", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-1Enx36NZorN3cve43GcIW+DkfESG9pcm0PS5K1F5rG/XKlAD2WytyyRJZQu+7pHaJ2RrOWVbT8qOQ1CRSS6Qcw=="],
+
+ "expo-splash-screen/xml2js": ["xml2js@0.6.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w=="],
+
"extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"fastembed/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
@@ -7209,6 +7365,10 @@
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+ "whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
+
+ "whatwg-url-without-unicode/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
+
"wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"wrangler/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
@@ -7743,6 +7903,10 @@
"engine.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
+ "expo-asset/@expo/image-utils/@expo/spawn-async": ["@expo/spawn-async@1.7.2", "", { "dependencies": { "cross-spawn": "^7.0.3" } }, "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew=="],
+
+ "expo-asset/@expo/image-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
"expo-asset/expo-constants/@expo/config": ["@expo/config@55.0.11", "", { "dependencies": { "@expo/config-plugins": "~55.0.7", "@expo/config-types": "^55.0.5", "@expo/json-file": "^10.0.12", "@expo/require-utils": "^55.0.3", "deepmerge": "^4.3.1", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "resolve-workspace-root": "^2.0.0", "semver": "^7.6.0", "slugify": "^1.3.4" } }, "sha512-14AkSmR1gOIUhCsPJ0cAo5ZduMNsPQsmFV9jBNZn1xC5Zb3D8x5eqvUie5QzWaUwdcyrq79uYJ2bTCiC6+nD0Q=="],
"expo-asset/expo-constants/@expo/env": ["@expo/env@2.1.1", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "getenv": "^2.0.0" } }, "sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg=="],
@@ -7751,6 +7915,14 @@
"expo-modules-autolinking/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+ "expo-splash-screen/@expo/config-plugins/@expo/config-types": ["@expo/config-types@56.0.0", "", {}, "sha512-9jwq4O09Sb9TBQaATCPrmElxkpN3t8LYk7QbYhtapgtNlZvObK9qkAuT02r3Z5Yin87V+dU3k0VmwiwOza8Paw=="],
+
+ "expo-splash-screen/@expo/config-plugins/@expo/json-file": ["@expo/json-file@10.1.0", "", { "dependencies": { "@babel/code-frame": "^7.20.0", "json5": "^2.2.3" } }, "sha512-3UBuXpYqr9DJRYHotRgqO8/5nsS6jJxK9MrLR/YRTHN4U1SKRz/hNEOmaQlrXaSd48cLutl4APDCy1mR52daUw=="],
+
+ "expo-splash-screen/@expo/config-plugins/@expo/plist": ["@expo/plist@0.6.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-Qn+rkyYXb+6B62l9zp8Q/FcCEPMibad51Erpzr0DmwHdS0pozzWV3iktiCoYkqHMOuIc9aIRNCTSs0IFleU0uQ=="],
+
+ "expo-splash-screen/@expo/config-plugins/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
"expo/expo-asset/@expo/image-utils": ["@expo/image-utils@0.10.0", "", { "dependencies": { "@expo/require-utils": "^56.1.2", "@expo/spawn-async": "^1.8.0", "chalk": "^4.0.0", "getenv": "^2.0.0", "jimp-compact": "0.16.1", "parse-png": "^2.1.0", "semver": "^7.6.0" } }, "sha512-iV1J+F5KpVqfdYsuot+5b8ZBDH6m/jQN2EzQSoa+qOmHqPNck17AihA4X3sso7ghn7p+AHeOKgftwT64amgmkQ=="],
"fastembed/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
@@ -8329,6 +8501,8 @@
"engine.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+ "expo-asset/@expo/image-utils/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
"expo-asset/expo-constants/@expo/config/@expo/config-plugins": ["@expo/config-plugins@55.0.7", "", { "dependencies": { "@expo/config-types": "^55.0.5", "@expo/json-file": "~10.0.12", "@expo/plist": "^0.5.2", "@expo/sdk-runtime-versions": "^1.0.0", "chalk": "^4.1.2", "debug": "^4.3.5", "getenv": "^2.0.0", "glob": "^13.0.0", "resolve-from": "^5.0.0", "semver": "^7.5.4", "slugify": "^1.6.6", "xcode": "^3.0.1", "xml2js": "0.6.0" } }, "sha512-XZUoDWrsHEkH3yasnDSJABM/UxP5a1ixzRwU/M+BToyn/f0nTrSJJe/Ay/FpxkI4JSNz2n0e06I23b2bleXKVA=="],
"expo-asset/expo-constants/@expo/config/@expo/config-types": ["@expo/config-types@55.0.5", "", {}, "sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg=="],
@@ -8341,6 +8515,10 @@
"expo-mcp/glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
+ "expo-splash-screen/@expo/config-plugins/@expo/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
+
+ "expo-splash-screen/@expo/config-plugins/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
"expo/expo-asset/@expo/image-utils/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"fastembed/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],