mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
feat(mobile): project status + priority pickers via formSheet routes
Project detail's Status and Priority chips were the last two picker chips still using the legacy centered-Modal pattern. The mixed gesture (Status/Priority popped a centered card; Lead / Add Resource slid up a formSheet) violated the picker-row consistency rule in CLAUDE.md Lesson 6 — the four chips on the same row now all open the same way. - New picker bodies under components/project/pickers/. - New formSheet routes under app/(app)/[workspace]/project/[id]/picker/. - Register both screens in workspace _layout.tsx using SHEET_OPTIONS. - project/[id].tsx: drop the local state, swap chip onPress to router.push, and remove the trailing 'still uses transparent-Modal' apology comment. - project/new.tsx is a draft modal so it can't push to a route (no project exists yet to read from cache). Inline a tiny DraftPickerModal shell that hosts the same picker bodies — documented in the file. - Delete the obsolete ProjectStatusPickerSheet / ProjectPriorityPickerSheet files and update rnr-migration.md to reflect that B.2 is closed.
This commit is contained in:
@@ -191,6 +191,14 @@ export default function WorkspaceLayout() {
|
||||
options={SHEET_OPTIONS}
|
||||
/>
|
||||
{/* Project-detail formSheet pickers. */}
|
||||
<Stack.Screen
|
||||
name="project/[id]/picker/status"
|
||||
options={SHEET_OPTIONS}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="project/[id]/picker/priority"
|
||||
options={SHEET_OPTIONS}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="project/[id]/picker/lead"
|
||||
options={SHEET_OPTIONS}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* confirmation via `Alert.alert` per iOS HIG (destructive actions need
|
||||
* a second tap).
|
||||
*/
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
ActionSheetIOS,
|
||||
ActivityIndicator,
|
||||
@@ -29,27 +29,18 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Stack, router, useLocalSearchParams } from "expo-router";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type {
|
||||
ProjectPriority,
|
||||
ProjectStatus,
|
||||
} from "@multica/core/types";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ProjectHeaderCard } from "@/components/project/project-header-card";
|
||||
import { ProjectPropertiesSection } from "@/components/project/project-properties-section";
|
||||
import { ProjectRelatedIssues } from "@/components/project/project-related-issues";
|
||||
import { ProjectResourcesSection } from "@/components/project/project-resources-section";
|
||||
import { ProjectStatusPickerSheet } from "@/components/project/pickers/project-status-picker-sheet";
|
||||
import { ProjectPriorityPickerSheet } from "@/components/project/pickers/project-priority-picker-sheet";
|
||||
import {
|
||||
projectDetailOptions,
|
||||
projectResourcesOptions,
|
||||
} from "@/data/queries/projects";
|
||||
import { issueKeys } from "@/data/queries/issue-keys";
|
||||
import {
|
||||
useDeleteProject,
|
||||
useUpdateProject,
|
||||
} from "@/data/mutations/projects";
|
||||
import { useDeleteProject } from "@/data/mutations/projects";
|
||||
import { useProjectRealtime } from "@/data/realtime/use-project-realtime";
|
||||
import { useWorkspaceStore } from "@/data/workspace-store";
|
||||
|
||||
@@ -60,16 +51,8 @@ export default function ProjectDetail() {
|
||||
const qc = useQueryClient();
|
||||
|
||||
const detail = useQuery(projectDetailOptions(wsId, id));
|
||||
const updateProject = useUpdateProject(id);
|
||||
const deleteProject = useDeleteProject(id);
|
||||
|
||||
// Status + Priority pickers still use the older transparent-Modal
|
||||
// pattern (project-status-picker-sheet / project-priority-picker-sheet) —
|
||||
// not part of the SheetShell formSheet migration. Lead + Add Resource
|
||||
// moved to formSheet routes.
|
||||
const [statusOpen, setStatusOpen] = useState(false);
|
||||
const [priorityOpen, setPriorityOpen] = useState(false);
|
||||
|
||||
// Per-record realtime — when another client deletes the project we're
|
||||
// viewing, pop back so the user isn't stranded on a 404.
|
||||
useProjectRealtime(id, () => router.back());
|
||||
@@ -195,8 +178,20 @@ export default function ProjectDetail() {
|
||||
/>
|
||||
<ProjectPropertiesSection
|
||||
project={project}
|
||||
onPressStatus={() => setStatusOpen(true)}
|
||||
onPressPriority={() => setPriorityOpen(true)}
|
||||
onPressStatus={() => {
|
||||
if (wsSlug)
|
||||
router.push({
|
||||
pathname: "/[workspace]/project/[id]/picker/status",
|
||||
params: { workspace: wsSlug, id },
|
||||
});
|
||||
}}
|
||||
onPressPriority={() => {
|
||||
if (wsSlug)
|
||||
router.push({
|
||||
pathname: "/[workspace]/project/[id]/picker/priority",
|
||||
params: { workspace: wsSlug, id },
|
||||
});
|
||||
}}
|
||||
onPressLead={() => {
|
||||
if (wsSlug)
|
||||
router.push({
|
||||
@@ -219,27 +214,6 @@ export default function ProjectDetail() {
|
||||
<ProjectRelatedIssues projectId={id} />
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{project ? (
|
||||
<>
|
||||
<ProjectStatusPickerSheet
|
||||
visible={statusOpen}
|
||||
value={project.status}
|
||||
onChange={(next: ProjectStatus) =>
|
||||
updateProject.mutate({ status: next })
|
||||
}
|
||||
onClose={() => setStatusOpen(false)}
|
||||
/>
|
||||
<ProjectPriorityPickerSheet
|
||||
visible={priorityOpen}
|
||||
value={project.priority}
|
||||
onChange={(next: ProjectPriority) =>
|
||||
updateProject.mutate({ priority: next })
|
||||
}
|
||||
onClose={() => setPriorityOpen(false)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Project priority picker route — presented as a formSheet by the parent
|
||||
* Stack. Self-contained: reads project from cache, fires useUpdateProject
|
||||
* on selection, then router.back()s.
|
||||
*/
|
||||
import { useLocalSearchParams, router } from "expo-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ProjectPriorityPickerBody } from "@/components/project/pickers/project-priority-picker-body";
|
||||
import { projectDetailOptions } from "@/data/queries/projects";
|
||||
import { useUpdateProject } from "@/data/mutations/projects";
|
||||
import { useWorkspaceStore } from "@/data/workspace-store";
|
||||
|
||||
export default function ProjectPriorityPickerRoute() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const wsId = useWorkspaceStore((s) => s.currentWorkspaceId);
|
||||
const { data: project } = useQuery(projectDetailOptions(wsId, id));
|
||||
const updateProject = useUpdateProject(id);
|
||||
|
||||
return (
|
||||
<ProjectPriorityPickerBody
|
||||
value={project?.priority ?? "none"}
|
||||
onChange={(next) => {
|
||||
updateProject.mutate({ priority: next });
|
||||
router.back();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Project status picker route — presented as a formSheet by the parent
|
||||
* Stack. Self-contained: reads project from cache, fires useUpdateProject
|
||||
* on selection, then router.back()s.
|
||||
*/
|
||||
import { useLocalSearchParams, router } from "expo-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ProjectStatusPickerBody } from "@/components/project/pickers/project-status-picker-body";
|
||||
import { projectDetailOptions } from "@/data/queries/projects";
|
||||
import { useUpdateProject } from "@/data/mutations/projects";
|
||||
import { useWorkspaceStore } from "@/data/workspace-store";
|
||||
|
||||
export default function ProjectStatusPickerRoute() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const wsId = useWorkspaceStore((s) => s.currentWorkspaceId);
|
||||
const { data: project } = useQuery(projectDetailOptions(wsId, id));
|
||||
const updateProject = useUpdateProject(id);
|
||||
|
||||
return (
|
||||
<ProjectStatusPickerBody
|
||||
value={project?.status ?? "planned"}
|
||||
onChange={(next) => {
|
||||
updateProject.mutate({ status: next });
|
||||
router.back();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Alert,
|
||||
InteractionManager,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
@@ -32,8 +33,8 @@ import {
|
||||
} from "@/components/ui/input-tokens";
|
||||
import { ProjectStatusIcon } from "@/components/ui/project-status-icon";
|
||||
import { ProjectPriorityIcon } from "@/components/ui/project-priority-icon";
|
||||
import { ProjectStatusPickerSheet } from "@/components/project/pickers/project-status-picker-sheet";
|
||||
import { ProjectPriorityPickerSheet } from "@/components/project/pickers/project-priority-picker-sheet";
|
||||
import { ProjectStatusPickerBody } from "@/components/project/pickers/project-status-picker-body";
|
||||
import { ProjectPriorityPickerBody } from "@/components/project/pickers/project-priority-picker-body";
|
||||
import {
|
||||
projectPriorityLabel,
|
||||
projectStatusLabel,
|
||||
@@ -212,22 +213,69 @@ export default function NewProject() {
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
<ProjectStatusPickerSheet
|
||||
visible={statusOpen}
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
onClose={() => setStatusOpen(false)}
|
||||
/>
|
||||
<ProjectPriorityPickerSheet
|
||||
<DraftPickerModal visible={statusOpen} onClose={() => setStatusOpen(false)}>
|
||||
<ProjectStatusPickerBody
|
||||
value={status}
|
||||
onChange={(next) => {
|
||||
setStatus(next);
|
||||
setStatusOpen(false);
|
||||
}}
|
||||
/>
|
||||
</DraftPickerModal>
|
||||
<DraftPickerModal
|
||||
visible={priorityOpen}
|
||||
value={priority}
|
||||
onChange={setPriority}
|
||||
onClose={() => setPriorityOpen(false)}
|
||||
/>
|
||||
>
|
||||
<ProjectPriorityPickerBody
|
||||
value={priority}
|
||||
onChange={(next) => {
|
||||
setPriority(next);
|
||||
setPriorityOpen(false);
|
||||
}}
|
||||
/>
|
||||
</DraftPickerModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline modal shell for the new-project draft pickers. The host screen is
|
||||
* already a presentation="modal", and routes can't read draft local state
|
||||
* (the project doesn't exist yet, nothing in the cache to push the picker
|
||||
* to). So we keep a lightweight centered card here rather than wire a
|
||||
* separate draft store for a single transient form — matches the carve-out
|
||||
* in apps/mobile/CLAUDE.md "modal container selection" for short fixed
|
||||
* picker lists with no neighbour pickers to be consistent with.
|
||||
*/
|
||||
function DraftPickerModal({
|
||||
visible,
|
||||
onClose,
|
||||
children,
|
||||
}: {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Pressable className="flex-1 bg-black/40" onPress={onClose}>
|
||||
<View className="flex-1 items-center justify-center px-8">
|
||||
<Pressable onPress={() => {}} className="w-full max-w-sm">
|
||||
<View className="bg-popover rounded-2xl overflow-hidden">
|
||||
{children}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
children,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Pure picker body for project priority — single-select over the 5
|
||||
* ProjectPriority enum values. See issue/pickers/status-picker-body.tsx for
|
||||
* the "extract body, route owns shell" rationale.
|
||||
*/
|
||||
import { Pressable, ScrollView, View } from "react-native";
|
||||
import type { ProjectPriority } from "@multica/core/types";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { ProjectPriorityIcon } from "@/components/ui/project-priority-icon";
|
||||
import {
|
||||
PROJECT_PRIORITIES,
|
||||
PROJECT_PRIORITY_LABEL,
|
||||
} from "@/lib/project-status";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
value: ProjectPriority | string;
|
||||
onChange: (next: ProjectPriority) => void;
|
||||
}
|
||||
|
||||
export function ProjectPriorityPickerBody({ value, onChange }: Props) {
|
||||
return (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View className="px-4 pt-3 pb-2">
|
||||
<Text className="text-lg font-semibold text-foreground">Priority</Text>
|
||||
</View>
|
||||
<View className="px-2">
|
||||
{PROJECT_PRIORITIES.map((priority) => {
|
||||
const selected = priority === value;
|
||||
return (
|
||||
<Pressable
|
||||
key={priority}
|
||||
onPress={() => onChange(priority)}
|
||||
className={cn(
|
||||
"flex-row items-center gap-3 rounded-lg px-3 py-3 active:bg-secondary",
|
||||
selected && "bg-secondary",
|
||||
)}
|
||||
>
|
||||
<ProjectPriorityIcon priority={priority} size={18} />
|
||||
<Text className="flex-1 text-base text-foreground">
|
||||
{PROJECT_PRIORITY_LABEL[priority]}
|
||||
</Text>
|
||||
{selected ? (
|
||||
<Text className="text-sm text-muted-foreground">✓</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Project priority picker. Single-select over 5 ProjectPriority enum values.
|
||||
* Shell mirrors project-status-picker-sheet.tsx.
|
||||
*/
|
||||
import { Modal, Pressable, View } from "react-native";
|
||||
import type { ProjectPriority } from "@multica/core/types";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { ProjectPriorityIcon } from "@/components/ui/project-priority-icon";
|
||||
import {
|
||||
PROJECT_PRIORITIES,
|
||||
PROJECT_PRIORITY_LABEL,
|
||||
} from "@/lib/project-status";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
value: ProjectPriority | string;
|
||||
onChange: (next: ProjectPriority) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ProjectPriorityPickerSheet({
|
||||
visible,
|
||||
value,
|
||||
onChange,
|
||||
onClose,
|
||||
}: Props) {
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Pressable className="flex-1 bg-black/40" onPress={onClose}>
|
||||
<View className="flex-1 items-center justify-center px-8">
|
||||
<Pressable onPress={() => {}} className="w-full max-w-sm">
|
||||
<View className="bg-popover rounded-2xl p-2">
|
||||
{PROJECT_PRIORITIES.map((priority) => {
|
||||
const selected = priority === value;
|
||||
return (
|
||||
<Pressable
|
||||
key={priority}
|
||||
onPress={() => {
|
||||
onChange(priority);
|
||||
onClose();
|
||||
}}
|
||||
className={cn(
|
||||
"flex-row items-center gap-3 rounded-lg px-3 py-2.5 active:bg-secondary",
|
||||
selected && "bg-secondary",
|
||||
)}
|
||||
>
|
||||
<ProjectPriorityIcon priority={priority} size={18} />
|
||||
<Text className="flex-1 text-sm text-foreground">
|
||||
{PROJECT_PRIORITY_LABEL[priority]}
|
||||
</Text>
|
||||
{selected ? (
|
||||
<Text className="text-xs text-muted-foreground">✓</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Pure picker body for project status — single-select over the 5
|
||||
* ProjectStatus enum values. See issue/pickers/status-picker-body.tsx for
|
||||
* the "extract body, route owns shell" rationale.
|
||||
*/
|
||||
import { Pressable, ScrollView, View } from "react-native";
|
||||
import type { ProjectStatus } from "@multica/core/types";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { ProjectStatusIcon } from "@/components/ui/project-status-icon";
|
||||
import {
|
||||
PROJECT_STATUSES,
|
||||
PROJECT_STATUS_LABEL,
|
||||
} from "@/lib/project-status";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
value: ProjectStatus | string;
|
||||
onChange: (next: ProjectStatus) => void;
|
||||
}
|
||||
|
||||
export function ProjectStatusPickerBody({ value, onChange }: Props) {
|
||||
return (
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View className="px-4 pt-3 pb-2">
|
||||
<Text className="text-lg font-semibold text-foreground">Status</Text>
|
||||
</View>
|
||||
<View className="px-2">
|
||||
{PROJECT_STATUSES.map((status) => {
|
||||
const selected = status === value;
|
||||
return (
|
||||
<Pressable
|
||||
key={status}
|
||||
onPress={() => onChange(status)}
|
||||
className={cn(
|
||||
"flex-row items-center gap-3 rounded-lg px-3 py-3 active:bg-secondary",
|
||||
selected && "bg-secondary",
|
||||
)}
|
||||
>
|
||||
<ProjectStatusIcon status={status} size={18} />
|
||||
<Text className="flex-1 text-base text-foreground">
|
||||
{PROJECT_STATUS_LABEL[status]}
|
||||
</Text>
|
||||
{selected ? (
|
||||
<Text className="text-sm text-muted-foreground">✓</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Project status picker. Single-select over the 5 ProjectStatus enum values.
|
||||
* Tap-to-apply (no confirm step); sheet auto-closes on selection.
|
||||
*
|
||||
* Modal shell mirrors issue/pickers/status-picker-sheet.tsx — same fade-in
|
||||
* centered popover, same tap-outside-to-dismiss behavior, same selected-row
|
||||
* styling.
|
||||
*/
|
||||
import { Modal, Pressable, View } from "react-native";
|
||||
import type { ProjectStatus } from "@multica/core/types";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { ProjectStatusIcon } from "@/components/ui/project-status-icon";
|
||||
import {
|
||||
PROJECT_STATUSES,
|
||||
PROJECT_STATUS_LABEL,
|
||||
} from "@/lib/project-status";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
value: ProjectStatus | string;
|
||||
onChange: (next: ProjectStatus) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ProjectStatusPickerSheet({
|
||||
visible,
|
||||
value,
|
||||
onChange,
|
||||
onClose,
|
||||
}: Props) {
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<Pressable className="flex-1 bg-black/40" onPress={onClose}>
|
||||
<View className="flex-1 items-center justify-center px-8">
|
||||
<Pressable onPress={() => {}} className="w-full max-w-sm">
|
||||
<View className="bg-popover rounded-2xl p-2">
|
||||
{PROJECT_STATUSES.map((status) => {
|
||||
const selected = status === value;
|
||||
return (
|
||||
<Pressable
|
||||
key={status}
|
||||
onPress={() => {
|
||||
onChange(status);
|
||||
onClose();
|
||||
}}
|
||||
className={cn(
|
||||
"flex-row items-center gap-3 rounded-lg px-3 py-2.5 active:bg-secondary",
|
||||
selected && "bg-secondary",
|
||||
)}
|
||||
>
|
||||
<ProjectStatusIcon status={status} size={18} />
|
||||
<Text className="flex-1 text-sm text-foreground">
|
||||
{PROJECT_STATUS_LABEL[status]}
|
||||
</Text>
|
||||
{selected ? (
|
||||
<Text className="text-xs text-muted-foreground">✓</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -105,18 +105,19 @@ This tier is where the biggest payoff lives (Lesson 6 in CLAUDE.md catalogues th
|
||||
| `components/issue/comment-action-sheet.tsx` | `ActionSheetIOS.showActionSheetWithOptions` | One-of-N action menu — exactly what ActionSheetIOS is for. Recommended Phase 3 starter (visible deletion, no styling questions). |
|
||||
| `components/issue/pickers/due-date-picker-sheet.tsx` | `@react-native-community/datetimepicker` inline picker | Date selection — native API already installed |
|
||||
|
||||
**B.2 — replaced by RNR `Select` (small file, mostly callsite changes)**
|
||||
**B.2 — replaced by formSheet routes (done)**
|
||||
|
||||
| Current sheet | Replacement |
|
||||
|---|---|
|
||||
| `components/issue/pickers/status-picker-sheet.tsx` | RNR `Select` |
|
||||
| `components/issue/pickers/priority-picker-sheet.tsx` | `ActionSheetIOS` (options fixed and few) or RNR `Select` |
|
||||
| `components/issue/pickers/assignee-picker-sheet.tsx` | RNR `Select` (searchable) |
|
||||
| `components/issue/pickers/label-picker-sheet.tsx` | RNR `Select` (multi) |
|
||||
| `components/issue/pickers/project-picker-sheet.tsx` | RNR `Select` (searchable) |
|
||||
| `components/project/pickers/project-status-picker-sheet.tsx` | RNR `Select` |
|
||||
| `components/project/pickers/project-priority-picker-sheet.tsx` | `ActionSheetIOS` or RNR `Select` |
|
||||
| `components/project/pickers/project-lead-picker-sheet.tsx` | RNR `Select` (searchable) |
|
||||
The original plan was to swap each picker-sheet for an RNR `Select`. The
|
||||
mobile-sheet-rollout PR series instead converged on a different shape:
|
||||
every former picker-sheet now ships as a pure `<XxxPickerBody>` component
|
||||
under `components/<domain>/pickers/`, embedded inside an Expo Router
|
||||
formSheet route at `app/(app)/[workspace]/<context>/picker/<field>.tsx`.
|
||||
This gives the iOS UISheetPresentationController-native chrome
|
||||
(grabber + detents + spring drag-dismiss) without the per-callsite
|
||||
state and visibility prop dance an RNR `Select` would still require.
|
||||
|
||||
Files in this row are all deleted; their bodies + routes live under the
|
||||
paths above. No follow-up needed.
|
||||
|
||||
**B.3 — genuinely needs a custom-content sheet (RNR `Dialog` pageSheet)**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user