mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
feat(mobile): redesign More popover — user card + lean nav
- Add user identity card at top of GlobalNavMenu, mirroring web sidebar dropdown (packages/views/layout/app-sidebar.tsx:496). Tap pushes into the existing settings page where account / workspaces / sign-out already live. - Trim NAV_ITEMS to Projects only. Inbox / My Issues / Chat are bottom tabs; Settings is reached via the user card. - Delete six orphaned stub routes (favorites, initiatives, views, teams, notifications, pins) — no remaining external references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/ui/text";
|
||||
|
||||
/**
|
||||
* Favorites placeholder. Real implementation deferred — list of pinned
|
||||
* issues / projects / views, mirroring the web Favorites surface.
|
||||
*/
|
||||
export default function FavoritesPage() {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background px-6">
|
||||
<Text className="text-sm text-muted-foreground text-center">
|
||||
Favorites coming soon.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/ui/text";
|
||||
|
||||
/**
|
||||
* Initiatives placeholder. Read-only list of workspace initiatives, filled
|
||||
* in a later phase to mirror the web Initiatives surface.
|
||||
*/
|
||||
export default function InitiativesPage() {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background px-6">
|
||||
<Text className="text-sm text-muted-foreground text-center">
|
||||
Initiatives coming soon.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/ui/text";
|
||||
|
||||
export default function NotificationsPage() {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background px-6">
|
||||
<Text className="text-sm text-muted-foreground text-center">
|
||||
Notification preferences coming soon.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/ui/text";
|
||||
|
||||
export default function PinsPage() {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background px-6">
|
||||
<Text className="text-sm text-muted-foreground text-center">
|
||||
Pins coming soon.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/ui/text";
|
||||
|
||||
/**
|
||||
* Teams placeholder. Workspace team list, filled in a later phase to mirror
|
||||
* the web Teams surface.
|
||||
*/
|
||||
export default function TeamsPage() {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background px-6">
|
||||
<Text className="text-sm text-muted-foreground text-center">
|
||||
Teams coming soon.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { View } from "react-native";
|
||||
import { Text } from "@/components/ui/text";
|
||||
|
||||
/**
|
||||
* Views placeholder. Saved-filter views surface, filled in a later phase to
|
||||
* mirror the web Views surface.
|
||||
*/
|
||||
export default function ViewsPage() {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-background px-6">
|
||||
<Text className="text-sm text-muted-foreground text-center">
|
||||
Views coming soon.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* GlobalNavMenu — top-right `…` popover that lets the user jump to any
|
||||
* top-level destination (Inbox, My Issues, Favorites, Projects, Initiatives,
|
||||
* Views, Teams, Settings, Search) and switch workspace.
|
||||
* GlobalNavMenu — bottom-right popover anchored above the More tab. Three
|
||||
* sections: user identity card → workspace switcher → real feature entries.
|
||||
*
|
||||
* Why a popover and not a tab: the iOS HIG treats tab-bar items as
|
||||
* destinations, not action triggers, so "More" was an anti-pattern. Linear /
|
||||
@@ -11,16 +10,30 @@
|
||||
* Reanimated v3 and the mobile app is on Reanimated v4. Same Modal+Pressable
|
||||
* pattern as status-picker-sheet.tsx etc. — keeps the dependency surface
|
||||
* untouched.
|
||||
*
|
||||
* Composition mirrors web's sidebar dropdown (packages/views/layout/
|
||||
* app-sidebar.tsx:496-511): user info row (avatar + name + email) sits above
|
||||
* the workspace list. On mobile the row is a tappable card that pushes into
|
||||
* the existing settings page, since there isn't enough screen real estate to
|
||||
* inline account / workspaces / sign-out the way web does.
|
||||
*/
|
||||
import { useMemo, useState } from "react";
|
||||
import { ActivityIndicator, Modal, Pressable, ScrollView, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
Modal,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Workspace } from "@multica/core/types";
|
||||
import type { User, Workspace } from "@multica/core/types";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { workspaceListOptions } from "@/data/queries/workspaces";
|
||||
import { useAuthStore } from "@/data/auth-store";
|
||||
import { useWorkspaceStore } from "@/data/workspace-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -31,16 +44,11 @@ interface NavItem {
|
||||
path: string;
|
||||
}
|
||||
|
||||
// Inbox / My Issues / Chat live on the bottom tab bar; Settings is reached
|
||||
// via the user card at the top of this popover. Only entries that are NOT
|
||||
// covered by either of those surfaces belong here.
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ label: "Inbox", icon: "mail-outline", path: "/inbox" },
|
||||
{ label: "My Issues", icon: "list-outline", path: "/my-issues" },
|
||||
{ label: "Favorites", icon: "star-outline", path: "/more/favorites" },
|
||||
{ label: "Projects", icon: "cube-outline", path: "/more/projects" },
|
||||
{ label: "Initiatives", icon: "navigate-outline", path: "/more/initiatives" },
|
||||
{ label: "Views", icon: "layers-outline", path: "/more/views" },
|
||||
{ label: "Teams", icon: "people-outline", path: "/more/teams" },
|
||||
{ label: "Settings", icon: "settings-outline", path: "/more/settings" },
|
||||
{ label: "Search", icon: "search-outline", path: "/search" },
|
||||
];
|
||||
|
||||
const ICON_COLOR = "#3f3f46";
|
||||
@@ -54,6 +62,7 @@ interface Props {
|
||||
export function GlobalNavMenu({ visible, onClose }: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const slug = useWorkspaceStore((s) => s.currentWorkspaceSlug);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const pathname = usePathname();
|
||||
const [showWorkspaces, setShowWorkspaces] = useState(false);
|
||||
|
||||
@@ -75,6 +84,13 @@ export function GlobalNavMenu({ visible, onClose }: Props) {
|
||||
router.push(`/${slug}${path}`);
|
||||
};
|
||||
|
||||
const onOpenSettings = () => {
|
||||
if (!slug) return;
|
||||
onClose();
|
||||
setShowWorkspaces(false);
|
||||
router.push(`/${slug}/more/settings`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
@@ -90,10 +106,11 @@ export function GlobalNavMenu({ visible, onClose }: Props) {
|
||||
}}
|
||||
>
|
||||
<View
|
||||
// Anchor under the top-right header. 8pt below safe-area top to
|
||||
// leave a hair of breathing room from the `…` trigger.
|
||||
style={{ paddingTop: insets.top + 56, paddingRight: 12 }}
|
||||
className="flex-1 items-end"
|
||||
// Anchor above the bottom tab bar (49pt iOS default + bottom
|
||||
// safe-area inset for the home indicator) with a hair of
|
||||
// breathing room. Menu rises from the More tab on the right.
|
||||
style={{ paddingBottom: insets.bottom + 49 + 8, paddingRight: 12 }}
|
||||
className="flex-1 items-end justify-end"
|
||||
>
|
||||
<Pressable onPress={() => {}}>
|
||||
<View
|
||||
@@ -107,6 +124,10 @@ export function GlobalNavMenu({ visible, onClose }: Props) {
|
||||
elevation: 8,
|
||||
}}
|
||||
>
|
||||
{/* User identity card — tap pushes into settings, where
|
||||
account info, workspace list, and sign out already live. */}
|
||||
<UserCard user={user} onPress={onOpenSettings} />
|
||||
|
||||
{/* Workspace switcher header */}
|
||||
<Pressable
|
||||
onPress={() => setShowWorkspaces((v) => !v)}
|
||||
@@ -240,3 +261,46 @@ function useCurrentWorkspace(slug: string | null): Workspace | undefined {
|
||||
[data, slug],
|
||||
);
|
||||
}
|
||||
|
||||
function UserCard({
|
||||
user,
|
||||
onPress,
|
||||
}: {
|
||||
user: User | null;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const initial = (user?.name ?? user?.email ?? "U").charAt(0).toUpperCase();
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
className="flex-row items-center gap-3 px-4 py-3.5 active:bg-secondary border-b border-border"
|
||||
>
|
||||
{user?.avatar_url ? (
|
||||
<Image
|
||||
source={{ uri: user.avatar_url }}
|
||||
className="size-10 rounded-full bg-muted"
|
||||
/>
|
||||
) : (
|
||||
<View className="size-10 rounded-full bg-muted items-center justify-center">
|
||||
<Text className="text-sm font-medium text-muted-foreground">
|
||||
{initial}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<View className="flex-1 min-w-0">
|
||||
<Text
|
||||
className="text-sm font-medium text-foreground"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{user?.name ?? "—"}
|
||||
</Text>
|
||||
{user?.email ? (
|
||||
<Text className="text-xs text-muted-foreground mt-0.5" numberOfLines={1}>
|
||||
{user.email}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={16} color={ICON_MUTED} />
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user