mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 18:52:39 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c54be00e99 |
@@ -30,8 +30,10 @@ The UI worklet owns transient motion:
|
|||||||
- the active gesture's starting revision
|
- the active gesture's starting revision
|
||||||
- the last settled target
|
- the last settled target
|
||||||
|
|
||||||
React also owns presentation lifecycle: whether an overlay is mounted/displayed and whether it may
|
React publishes the active panel only after its motion reaches the final anchor. Retained content
|
||||||
receive pointer events. Worklets never own `display` or `pointerEvents`.
|
never observes gesture previews, progress, or an unsettled target. The gesture hosts stay mounted;
|
||||||
|
worklets reveal their retained overlays through native styles while React owns pointer events and
|
||||||
|
accessibility.
|
||||||
|
|
||||||
## Why one position
|
## Why one position
|
||||||
|
|
||||||
@@ -85,8 +87,8 @@ definition, no longer eligible to begin.
|
|||||||
- Animated panel nodes use React Native static styles plus inline theme values. Do not attach
|
- Animated panel nodes use React Native static styles plus inline theme values. Do not attach
|
||||||
Unistyles-generated styles to those nodes; Unistyles and Reanimated patching the same Fabric node
|
Unistyles-generated styles to those nodes; Unistyles and Reanimated patching the same Fabric node
|
||||||
has caused native crashes.
|
has caused native crashes.
|
||||||
- The plain React wrapper owns `display: none` after settlement. This prevents a stale Fabric animated
|
- Gesture start and progress must not update React state. The retained overlay is already mounted and
|
||||||
prop commit from resurrecting a closed overlay.
|
offscreen; only the shared position and its derived native styles move during a drag.
|
||||||
- Hidden tabs and workspaces use `RetainedPanel`. It owns a non-collapsible native root, visibility,
|
- Hidden tabs and workspaces use `RetainedPanel`. It owns a non-collapsible native root, visibility,
|
||||||
pointer events, and the active signal consumed by `useRetainedPanelActive`.
|
pointer events, and the active signal consumed by `useRetainedPanelActive`.
|
||||||
- Panels whose gesture wrapper already owns visibility use `RetainedPanelActivity` to provide the
|
- Panels whose gesture wrapper already owns visibility use `RetainedPanelActivity` to provide the
|
||||||
@@ -98,8 +100,8 @@ definition, no longer eligible to begin.
|
|||||||
- Retention order and render order are separate concerns. LRU metadata may change on every switch;
|
- Retention order and render order are separate concerns. LRU metadata may change on every switch;
|
||||||
keyed retained roots must keep a stable sibling order. Moving large retained roots triggered Fabric
|
keyed retained roots must keep a stable sibling order. Moving large retained roots triggered Fabric
|
||||||
Differ failures (`addViewAt` / `removeViewAt` view reuse) on Android.
|
Differ failures (`addViewAt` / `removeViewAt` view reuse) on Android.
|
||||||
- The newly active panel must be included in the same render that changes selection. Adding it from an
|
- `useIsMobilePanelActive` follows the settled target, not the requested target. Activation work may
|
||||||
effect creates a committed frame where every retained panel is hidden, which is a real blank screen.
|
begin only after the finishing animation completes; cancellation publishes nothing.
|
||||||
- Do not suspend retained native subtrees with `Suspense`/`react-freeze`. Suspension changes native
|
- Do not suspend retained native subtrees with `Suspense`/`react-freeze`. Suspension changes native
|
||||||
ownership and can detach descendants. Keep the tree mounted, stabilize its subscriptions/selectors,
|
ownership and can detach descendants. Keep the tree mounted, stabilize its subscriptions/selectors,
|
||||||
and use the retained-panel active signal to stop timers, polling, and other genuine background work.
|
and use the retained-panel active signal to stop timers, polling, and other genuine background work.
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ import { useOpenProject } from "@/hooks/use-open-project";
|
|||||||
import { useAppSettings } from "@/hooks/use-settings";
|
import { useAppSettings } from "@/hooks/use-settings";
|
||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import { useOpenAgentListGesture } from "@/mobile-panels/gestures";
|
import { useOpenAgentListGesture } from "@/mobile-panels/gestures";
|
||||||
import { MobilePanelsProvider } from "@/mobile-panels/provider";
|
import { MobilePanelsProvider, useIsMobilePanelActive } from "@/mobile-panels/provider";
|
||||||
import { I18nProvider } from "@/i18n/provider";
|
import { I18nProvider } from "@/i18n/provider";
|
||||||
import {
|
import {
|
||||||
KeyboardActionDispatcherProvider,
|
KeyboardActionDispatcherProvider,
|
||||||
@@ -114,7 +114,7 @@ import {
|
|||||||
useHosts,
|
useHosts,
|
||||||
} from "@/runtime/host-runtime";
|
} from "@/runtime/host-runtime";
|
||||||
import { getDaemonStartService } from "@/runtime/daemon-start-service";
|
import { getDaemonStartService } from "@/runtime/daemon-start-service";
|
||||||
import { selectIsAgentListOpen, usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { flushDraftPersistStorage } from "@/stores/draft-store";
|
import { flushDraftPersistStorage } from "@/stores/draft-store";
|
||||||
import { getNextThemePreference } from "@/styles/theme";
|
import { getNextThemePreference } from "@/styles/theme";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
@@ -627,10 +627,9 @@ function SidebarChrome({
|
|||||||
keyboardShortcutsEnabled: boolean;
|
keyboardShortcutsEnabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const isCompactLayout = useIsCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
const isOpen = usePanelStore((state) =>
|
const isMobileActive = useIsMobilePanelActive("agent-list");
|
||||||
selectIsAgentListOpen(state, { isCompact: isCompactLayout }),
|
const isDesktopOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||||
);
|
const active = visible && (isCompactLayout ? isMobileActive : isDesktopOpen);
|
||||||
const active = visible && isOpen;
|
|
||||||
return (
|
return (
|
||||||
<SidebarModelProvider active={active}>
|
<SidebarModelProvider active={active}>
|
||||||
{mounted ? <LeftSidebar active={active} /> : null}
|
{mounted ? <LeftSidebar active={active} /> : null}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
NativeExplorerSidebarDock,
|
NativeExplorerSidebarDock,
|
||||||
} from "@/components/compact-explorer-sidebar";
|
} from "@/components/compact-explorer-sidebar";
|
||||||
import { useOpenFileExplorerGesture } from "@/mobile-panels/gestures";
|
import { useOpenFileExplorerGesture } from "@/mobile-panels/gestures";
|
||||||
|
import { useIsMobilePanelActive } from "@/mobile-panels/provider";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import { selectIsCompactFileExplorerOpen, usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||||
import type { WorkspaceTabTarget } from "@/workspace-tabs/model";
|
import type { WorkspaceTabTarget } from "@/workspace-tabs/model";
|
||||||
import { useWorkspaceCheckoutStatus } from "@/screens/workspace/use-workspace-checkout-status";
|
import { useWorkspaceCheckoutStatus } from "@/screens/workspace/use-workspace-checkout-status";
|
||||||
@@ -51,7 +52,7 @@ function useActiveCompactExplorerSidebarModel(
|
|||||||
): CompactExplorerSidebarHostModel | null {
|
): CompactExplorerSidebarHostModel | null {
|
||||||
const selection = useActiveWorkspaceSelection();
|
const selection = useActiveWorkspaceSelection();
|
||||||
const workspace = useWorkspace(selection?.serverId ?? null, selection?.workspaceId ?? null);
|
const workspace = useWorkspace(selection?.serverId ?? null, selection?.workspaceId ?? null);
|
||||||
const isExplorerOpen = usePanelStore(selectIsCompactFileExplorerOpen);
|
const isExplorerActive = useIsMobilePanelActive("file-explorer");
|
||||||
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
||||||
const client = useHostRuntimeClient(selection?.serverId ?? "");
|
const client = useHostRuntimeClient(selection?.serverId ?? "");
|
||||||
const isConnected = useHostRuntimeIsConnected(selection?.serverId ?? "");
|
const isConnected = useHostRuntimeIsConnected(selection?.serverId ?? "");
|
||||||
@@ -67,32 +68,32 @@ function useActiveCompactExplorerSidebarModel(
|
|||||||
const resolvedModel = useMemo(
|
const resolvedModel = useMemo(
|
||||||
() =>
|
() =>
|
||||||
resolveCompactExplorerSidebarHostModel({
|
resolveCompactExplorerSidebarHostModel({
|
||||||
previous: isExplorerOpen ? retainedModelRef.current : null,
|
previous: isExplorerActive ? retainedModelRef.current : null,
|
||||||
selection,
|
selection,
|
||||||
workspace,
|
workspace,
|
||||||
isGit: checkoutQuery.data?.isGit ?? false,
|
isGit: checkoutQuery.data?.isGit ?? false,
|
||||||
}),
|
}),
|
||||||
[checkoutQuery.data?.isGit, isExplorerOpen, selection, workspace],
|
[checkoutQuery.data?.isGit, isExplorerActive, selection, workspace],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selection) {
|
if (!selection) {
|
||||||
retainedModelRef.current = null;
|
retainedModelRef.current = null;
|
||||||
if (enabled && isExplorerOpen) {
|
if (enabled && isExplorerActive) {
|
||||||
showMobileAgent();
|
showMobileAgent();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isExplorerOpen) {
|
if (!isExplorerActive) {
|
||||||
retainedModelRef.current = null;
|
retainedModelRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resolvedModel) {
|
if (resolvedModel) {
|
||||||
retainedModelRef.current = resolvedModel;
|
retainedModelRef.current = resolvedModel;
|
||||||
}
|
}
|
||||||
}, [enabled, isExplorerOpen, resolvedModel, selection, showMobileAgent]);
|
}, [enabled, isExplorerActive, resolvedModel, selection, showMobileAgent]);
|
||||||
|
|
||||||
return selection ? (resolvedModel ?? (isExplorerOpen ? retainedModelRef.current : null)) : null;
|
return selection ? (resolvedModel ?? (isExplorerActive ? retainedModelRef.current : null)) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CompactExplorerSidebarHostProps {
|
interface CompactExplorerSidebarHostProps {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
type ExplorerTab,
|
type ExplorerTab,
|
||||||
} from "@/stores/panel-store";
|
} from "@/stores/panel-store";
|
||||||
import { useCloseFileExplorerGesture } from "@/mobile-panels/gestures";
|
import { useCloseFileExplorerGesture } from "@/mobile-panels/gestures";
|
||||||
|
import { useIsMobilePanelActive } from "@/mobile-panels/provider";
|
||||||
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
||||||
import {
|
import {
|
||||||
HEADER_INNER_HEIGHT,
|
HEADER_INNER_HEIGHT,
|
||||||
@@ -81,7 +82,7 @@ export function CompactExplorerSidebar({
|
|||||||
}: ExplorerSidebarProps) {
|
}: ExplorerSidebarProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const isOpen = usePanelStore(selectIsCompactFileExplorerOpen);
|
const isActive = useIsMobilePanelActive("file-explorer");
|
||||||
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
||||||
const { explorerTab, handleTabPress } = useExplorerSidebarSharedState({
|
const { explorerTab, handleTabPress } = useExplorerSidebarSharedState({
|
||||||
serverId,
|
serverId,
|
||||||
@@ -99,11 +100,11 @@ export function CompactExplorerSidebar({
|
|||||||
(reason: string) => {
|
(reason: string) => {
|
||||||
logExplorerSidebar("handleClose", {
|
logExplorerSidebar("handleClose", {
|
||||||
reason,
|
reason,
|
||||||
isOpen,
|
isOpen: isActive,
|
||||||
});
|
});
|
||||||
showMobileAgent();
|
showMobileAgent();
|
||||||
},
|
},
|
||||||
[isOpen, showMobileAgent],
|
[isActive, showMobileAgent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleHeaderClose = useCallback(() => handleClose("header-close-button"), [handleClose]);
|
const handleHeaderClose = useCallback(() => handleClose("header-close-button"), [handleClose]);
|
||||||
@@ -127,7 +128,7 @@ export function CompactExplorerSidebar({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RetainedPanelActivity active={isOpen}>
|
<RetainedPanelActivity active={isActive}>
|
||||||
<MobilePanelOverlay
|
<MobilePanelOverlay
|
||||||
panel="file-explorer"
|
panel="file-explorer"
|
||||||
closeGesture={closeGesture}
|
closeGesture={closeGesture}
|
||||||
@@ -141,7 +142,7 @@ export function CompactExplorerSidebar({
|
|||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
workspaceRoot={workspaceRoot}
|
workspaceRoot={workspaceRoot}
|
||||||
isGit={isGit}
|
isGit={isGit}
|
||||||
isOpen={isOpen}
|
isOpen={isActive}
|
||||||
onOpenFile={onOpenFile}
|
onOpenFile={onOpenFile}
|
||||||
/>
|
/>
|
||||||
</MobilePanelOverlay>
|
</MobilePanelOverlay>
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ import { usePanelStore } from "@/stores/panel-store";
|
|||||||
import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window";
|
import { useOwnsWindowChromeCorner, WindowChromeSafeArea } from "@/utils/desktop-window";
|
||||||
import { useCloseAgentListGesture } from "@/mobile-panels/gestures";
|
import { useCloseAgentListGesture } from "@/mobile-panels/gestures";
|
||||||
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
||||||
import { useIsMobilePanelPresented } from "@/mobile-panels/provider";
|
|
||||||
import {
|
import {
|
||||||
buildOpenProjectRoute,
|
buildOpenProjectRoute,
|
||||||
buildNewWorkspaceRoute,
|
buildNewWorkspaceRoute,
|
||||||
@@ -121,6 +120,7 @@ interface SidebarLabels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MobileSidebarProps extends SidebarSharedProps {
|
interface MobileSidebarProps extends SidebarSharedProps {
|
||||||
|
active: boolean;
|
||||||
insetsTop: number;
|
insetsTop: number;
|
||||||
insetsBottom: number;
|
insetsBottom: number;
|
||||||
closeSidebar: () => void;
|
closeSidebar: () => void;
|
||||||
@@ -273,6 +273,7 @@ export const LeftSidebar = memo(function LeftSidebar({ active }: { active: boole
|
|||||||
<RetainedPanelActivity active={active}>
|
<RetainedPanelActivity active={active}>
|
||||||
<MobileSidebar
|
<MobileSidebar
|
||||||
{...sharedProps}
|
{...sharedProps}
|
||||||
|
active={active}
|
||||||
insetsTop={insets.top}
|
insetsTop={insets.top}
|
||||||
insetsBottom={insets.bottom}
|
insetsBottom={insets.bottom}
|
||||||
closeSidebar={showMobileAgent}
|
closeSidebar={showMobileAgent}
|
||||||
@@ -604,6 +605,7 @@ function SidebarFooter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MobileSidebar({
|
function MobileSidebar({
|
||||||
|
active,
|
||||||
theme,
|
theme,
|
||||||
workspaceGroups,
|
workspaceGroups,
|
||||||
projectIconTargets,
|
projectIconTargets,
|
||||||
@@ -638,7 +640,6 @@ function MobileSidebar({
|
|||||||
const isSessionsActive = pathname.includes("/sessions");
|
const isSessionsActive = pathname.includes("/sessions");
|
||||||
const isSchedulesActive = pathname.includes("/schedules");
|
const isSchedulesActive = pathname.includes("/schedules");
|
||||||
const { gesture: closeGesture, gestureRef: closeGestureRef } = useCloseAgentListGesture();
|
const { gesture: closeGesture, gestureRef: closeGestureRef } = useCloseAgentListGesture();
|
||||||
const dragGestureHostPresented = useIsMobilePanelPresented("agent-list");
|
|
||||||
|
|
||||||
const handleViewMore = useCallback(() => {
|
const handleViewMore = useCallback(() => {
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
@@ -737,7 +738,7 @@ function MobileSidebar({
|
|||||||
onWorkspacePress={handleWorkspacePress}
|
onWorkspacePress={handleWorkspacePress}
|
||||||
onAddProject={handleOpenProject}
|
onAddProject={handleOpenProject}
|
||||||
parentGestureRef={closeGestureRef}
|
parentGestureRef={closeGestureRef}
|
||||||
dragGestureHostPresented={dragGestureHostPresented}
|
dragGestureHostActive={active}
|
||||||
listHeaderComponent={workspacesSectionHeaderElement}
|
listHeaderComponent={workspacesSectionHeaderElement}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ interface SidebarWorkspaceListProps {
|
|||||||
listHeaderComponent?: ReactElement | null;
|
listHeaderComponent?: ReactElement | null;
|
||||||
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
||||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||||
dragGestureHostPresented?: boolean;
|
dragGestureHostActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectHeaderRowProps {
|
interface ProjectHeaderRowProps {
|
||||||
@@ -1540,7 +1540,7 @@ function ProjectBlock({
|
|||||||
isDragging,
|
isDragging,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
useNestable,
|
useNestable,
|
||||||
dragGestureHostPresented,
|
dragGestureHostActive,
|
||||||
creatingWorkspaceIds,
|
creatingWorkspaceIds,
|
||||||
activeWorkspaceSelection,
|
activeWorkspaceSelection,
|
||||||
hostBadgeByServerId,
|
hostBadgeByServerId,
|
||||||
@@ -1565,7 +1565,7 @@ function ProjectBlock({
|
|||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
dragHandleProps?: DraggableListDragHandleProps;
|
dragHandleProps?: DraggableListDragHandleProps;
|
||||||
useNestable: boolean;
|
useNestable: boolean;
|
||||||
dragGestureHostPresented?: boolean;
|
dragGestureHostActive?: boolean;
|
||||||
creatingWorkspaceIds: ReadonlySet<string>;
|
creatingWorkspaceIds: ReadonlySet<string>;
|
||||||
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||||
hostBadgeByServerId: ReadonlyMap<string, HostBadgeModel>;
|
hostBadgeByServerId: ReadonlyMap<string, HostBadgeModel>;
|
||||||
@@ -1745,7 +1745,7 @@ function ProjectBlock({
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
nestable={useNestable}
|
nestable={useNestable}
|
||||||
simultaneousGestureRef={parentGestureRef}
|
simultaneousGestureRef={parentGestureRef}
|
||||||
gestureHostPresented={dragGestureHostPresented}
|
gestureHostPresented={dragGestureHostActive}
|
||||||
containerStyle={styles.workspaceListContainer}
|
containerStyle={styles.workspaceListContainer}
|
||||||
/>
|
/>
|
||||||
{canToggleWorkspaces ? (
|
{canToggleWorkspaces ? (
|
||||||
@@ -1829,7 +1829,7 @@ function areProjectBlockPropsEqual(previous: ProjectBlockProps, next: ProjectBlo
|
|||||||
previous.isDragging === next.isDragging &&
|
previous.isDragging === next.isDragging &&
|
||||||
previous.dragHandleProps === next.dragHandleProps &&
|
previous.dragHandleProps === next.dragHandleProps &&
|
||||||
previous.useNestable === next.useNestable &&
|
previous.useNestable === next.useNestable &&
|
||||||
previous.dragGestureHostPresented === next.dragGestureHostPresented &&
|
previous.dragGestureHostActive === next.dragGestureHostActive &&
|
||||||
previous.creatingWorkspaceIds === next.creatingWorkspaceIds &&
|
previous.creatingWorkspaceIds === next.creatingWorkspaceIds &&
|
||||||
areProjectBlockSelectionsEqual(previous, next)
|
areProjectBlockSelectionsEqual(previous, next)
|
||||||
);
|
);
|
||||||
@@ -1882,7 +1882,7 @@ export function SidebarWorkspaceList({
|
|||||||
listFooterComponent,
|
listFooterComponent,
|
||||||
listHeaderComponent,
|
listHeaderComponent,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
dragGestureHostPresented,
|
dragGestureHostActive,
|
||||||
}: SidebarWorkspaceListProps) {
|
}: SidebarWorkspaceListProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const hosts = useHosts();
|
const hosts = useHosts();
|
||||||
@@ -1959,7 +1959,7 @@ export function SidebarWorkspaceList({
|
|||||||
listHeaderComponent={listHeaderComponent}
|
listHeaderComponent={listHeaderComponent}
|
||||||
sidebarFilterEmpty={sidebarFilterEmpty}
|
sidebarFilterEmpty={sidebarFilterEmpty}
|
||||||
parentGestureRef={parentGestureRef}
|
parentGestureRef={parentGestureRef}
|
||||||
dragGestureHostPresented={dragGestureHostPresented}
|
dragGestureHostActive={dragGestureHostActive}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProjectModeList
|
<ProjectModeList
|
||||||
@@ -1977,7 +1977,7 @@ export function SidebarWorkspaceList({
|
|||||||
sidebarFilterEmpty={sidebarFilterEmpty}
|
sidebarFilterEmpty={sidebarFilterEmpty}
|
||||||
hasActiveProjectFilter={hasActiveProjectFilter}
|
hasActiveProjectFilter={hasActiveProjectFilter}
|
||||||
parentGestureRef={parentGestureRef}
|
parentGestureRef={parentGestureRef}
|
||||||
dragGestureHostPresented={dragGestureHostPresented}
|
dragGestureHostActive={dragGestureHostActive}
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
hostBadgeByServerId={hostBadgeByServerId}
|
hostBadgeByServerId={hostBadgeByServerId}
|
||||||
supportsMultiplicityByServerId={supportsMultiplicityByServerId}
|
supportsMultiplicityByServerId={supportsMultiplicityByServerId}
|
||||||
@@ -2010,7 +2010,7 @@ function SidebarGroupedModeList({
|
|||||||
listHeaderComponent,
|
listHeaderComponent,
|
||||||
sidebarFilterEmpty,
|
sidebarFilterEmpty,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
dragGestureHostPresented,
|
dragGestureHostActive,
|
||||||
}: {
|
}: {
|
||||||
workspaceGroups: SidebarWorkspaceGroup[];
|
workspaceGroups: SidebarWorkspaceGroup[];
|
||||||
pinnedGroups: PinnedSidebarGroups;
|
pinnedGroups: PinnedSidebarGroups;
|
||||||
@@ -2025,7 +2025,7 @@ function SidebarGroupedModeList({
|
|||||||
listHeaderComponent?: ReactElement | null;
|
listHeaderComponent?: ReactElement | null;
|
||||||
sidebarFilterEmpty: boolean;
|
sidebarFilterEmpty: boolean;
|
||||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||||
dragGestureHostPresented?: boolean;
|
dragGestureHostActive?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const showShortcutBadges = useShowShortcutBadges();
|
const showShortcutBadges = useShowShortcutBadges();
|
||||||
const pinnedWorkspaces = useMemo(
|
const pinnedWorkspaces = useMemo(
|
||||||
@@ -2052,7 +2052,7 @@ function SidebarGroupedModeList({
|
|||||||
listHeaderComponent={listHeaderComponent}
|
listHeaderComponent={listHeaderComponent}
|
||||||
sidebarFilterEmpty={sidebarFilterEmpty}
|
sidebarFilterEmpty={sidebarFilterEmpty}
|
||||||
parentGestureRef={parentGestureRef}
|
parentGestureRef={parentGestureRef}
|
||||||
dragGestureHostPresented={dragGestureHostPresented}
|
dragGestureHostActive={dragGestureHostActive}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2072,7 +2072,7 @@ function ProjectModeList({
|
|||||||
sidebarFilterEmpty,
|
sidebarFilterEmpty,
|
||||||
hasActiveProjectFilter,
|
hasActiveProjectFilter,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
dragGestureHostPresented,
|
dragGestureHostActive,
|
||||||
pathname,
|
pathname,
|
||||||
hostBadgeByServerId,
|
hostBadgeByServerId,
|
||||||
supportsMultiplicityByServerId,
|
supportsMultiplicityByServerId,
|
||||||
@@ -2289,7 +2289,7 @@ function ProjectModeList({
|
|||||||
isDragging={dragState.isDragging}
|
isDragging={dragState.isDragging}
|
||||||
dragHandleProps={dragState.dragHandleProps}
|
dragHandleProps={dragState.dragHandleProps}
|
||||||
useNestable={platformIsNative}
|
useNestable={platformIsNative}
|
||||||
dragGestureHostPresented={dragGestureHostPresented}
|
dragGestureHostActive={dragGestureHostActive}
|
||||||
creatingWorkspaceIds={creatingWorkspaceIds}
|
creatingWorkspaceIds={creatingWorkspaceIds}
|
||||||
activeWorkspaceSelection={activeWorkspaceSelection}
|
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||||
hostBadgeByServerId={hostBadgeByServerId}
|
hostBadgeByServerId={hostBadgeByServerId}
|
||||||
@@ -2311,7 +2311,7 @@ function ProjectModeList({
|
|||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
onToggleProjectCollapsed,
|
onToggleProjectCollapsed,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
dragGestureHostPresented,
|
dragGestureHostActive,
|
||||||
projectIconByProjectViewKey,
|
projectIconByProjectViewKey,
|
||||||
selectionEnabled,
|
selectionEnabled,
|
||||||
shortcutIndexByWorkspaceKey,
|
shortcutIndexByWorkspaceKey,
|
||||||
@@ -2388,7 +2388,7 @@ function ProjectModeList({
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
nestable={platformIsNative}
|
nestable={platformIsNative}
|
||||||
simultaneousGestureRef={parentGestureRef}
|
simultaneousGestureRef={parentGestureRef}
|
||||||
gestureHostPresented={dragGestureHostPresented}
|
gestureHostPresented={dragGestureHostActive}
|
||||||
containerStyle={styles.projectListContainer}
|
containerStyle={styles.projectListContainer}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -2411,7 +2411,7 @@ function ProjectModeList({
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
nestable={platformIsNative}
|
nestable={platformIsNative}
|
||||||
simultaneousGestureRef={parentGestureRef}
|
simultaneousGestureRef={parentGestureRef}
|
||||||
gestureHostPresented={dragGestureHostPresented}
|
gestureHostPresented={dragGestureHostActive}
|
||||||
containerStyle={styles.workspaceListContainer}
|
containerStyle={styles.workspaceListContainer}
|
||||||
/>
|
/>
|
||||||
{canTogglePinnedChats ? (
|
{canTogglePinnedChats ? (
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ interface StatusWorkspaceListProps {
|
|||||||
/** Swaps the group list for the label filter's empty state. Never the header above it. */
|
/** Swaps the group list for the label filter's empty state. Never the header above it. */
|
||||||
sidebarFilterEmpty?: boolean;
|
sidebarFilterEmpty?: boolean;
|
||||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||||
dragGestureHostPresented?: boolean;
|
dragGestureHostActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarStatusWorkspaceList({
|
export function SidebarStatusWorkspaceList({
|
||||||
@@ -143,7 +143,7 @@ export function SidebarStatusWorkspaceList({
|
|||||||
listHeaderComponent,
|
listHeaderComponent,
|
||||||
sidebarFilterEmpty = false,
|
sidebarFilterEmpty = false,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
dragGestureHostPresented,
|
dragGestureHostActive,
|
||||||
}: StatusWorkspaceListProps) {
|
}: StatusWorkspaceListProps) {
|
||||||
const collapsedWorkspaceGroupKeys = useSidebarCollapsedSectionsStore(
|
const collapsedWorkspaceGroupKeys = useSidebarCollapsedSectionsStore(
|
||||||
(state) => state.collapsedWorkspaceGroupKeys,
|
(state) => state.collapsedWorkspaceGroupKeys,
|
||||||
@@ -214,7 +214,7 @@ export function SidebarStatusWorkspaceList({
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
nestable={platformIsNative}
|
nestable={platformIsNative}
|
||||||
simultaneousGestureRef={parentGestureRef}
|
simultaneousGestureRef={parentGestureRef}
|
||||||
gestureHostPresented={dragGestureHostPresented}
|
gestureHostPresented={dragGestureHostActive}
|
||||||
/>
|
/>
|
||||||
{canTogglePinnedWorkspaces ? (
|
{canTogglePinnedWorkspaces ? (
|
||||||
<SidebarGroupToggleRow
|
<SidebarGroupToggleRow
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function useOpenAgentListGesture(enabled: boolean) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
startedRevision.value = beginGesture({ origin: "agent", preview: "agent-list" });
|
startedRevision.value = beginGesture({ origin: "agent" });
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
updateGesture(startedRevision.value, -event.translationX / windowWidth);
|
updateGesture(startedRevision.value, -event.translationX / windowWidth);
|
||||||
@@ -188,7 +188,6 @@ export function useCloseAgentListGesture() {
|
|||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
startedRevision.value = beginGesture({
|
startedRevision.value = beginGesture({
|
||||||
origin: "agent-list",
|
origin: "agent-list",
|
||||||
preview: "agent-list",
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
@@ -290,7 +289,6 @@ export function useOpenFileExplorerGesture({ enabled, onOpen }: OpenFileExplorer
|
|||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
startedRevision.value = beginGesture({
|
startedRevision.value = beginGesture({
|
||||||
origin: "agent",
|
origin: "agent",
|
||||||
preview: "file-explorer",
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
@@ -386,7 +384,6 @@ export function useCloseFileExplorerGesture() {
|
|||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
startedRevision.value = beginGesture({
|
startedRevision.value = beginGesture({
|
||||||
origin: "file-explorer",
|
origin: "file-explorer",
|
||||||
preview: "file-explorer",
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
canBeginMobilePanelGesture,
|
canBeginMobilePanelGesture,
|
||||||
createMobilePanelMotionState,
|
createMobilePanelMotionState,
|
||||||
getMobilePanelFrame,
|
getMobilePanelFrame,
|
||||||
|
isMobilePanelActive,
|
||||||
isMobilePanelGestureCurrent,
|
isMobilePanelGestureCurrent,
|
||||||
transitionMobilePanel,
|
transitionMobilePanel,
|
||||||
type MobilePanelCommit,
|
type MobilePanelCommit,
|
||||||
@@ -77,6 +78,71 @@ class MobilePanelsScenario {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("mobile panel ownership", () => {
|
describe("mobile panel ownership", () => {
|
||||||
|
it("publishes activity only after motion settles", () => {
|
||||||
|
const initial = createMobilePanelMotionState({ target: "agent", revision: 0 });
|
||||||
|
const dragging = transitionMobilePanel(initial, {
|
||||||
|
type: "gesture.begin",
|
||||||
|
origin: "agent",
|
||||||
|
}).state;
|
||||||
|
const released = transitionMobilePanel(dragging, {
|
||||||
|
type: "gesture.finish",
|
||||||
|
startedRevision: 0,
|
||||||
|
success: true,
|
||||||
|
target: "agent-list",
|
||||||
|
}).state;
|
||||||
|
const commanded = transitionMobilePanel(released, {
|
||||||
|
type: "command",
|
||||||
|
selection: { target: "agent-list", revision: 1 },
|
||||||
|
}).state;
|
||||||
|
|
||||||
|
expect(isMobilePanelActive(dragging, "agent-list")).toBe(false);
|
||||||
|
expect(isMobilePanelActive(released, "agent-list")).toBe(false);
|
||||||
|
expect(isMobilePanelActive(commanded, "agent-list")).toBe(false);
|
||||||
|
|
||||||
|
const settled = transitionMobilePanel(commanded, {
|
||||||
|
type: "animation.finished",
|
||||||
|
revision: 1,
|
||||||
|
target: "agent-list",
|
||||||
|
}).state;
|
||||||
|
expect(isMobilePanelActive(settled, "agent-list")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not change activity for a cancelled preview", () => {
|
||||||
|
const initial = createMobilePanelMotionState({ target: "agent", revision: 0 });
|
||||||
|
const dragging = transitionMobilePanel(initial, {
|
||||||
|
type: "gesture.begin",
|
||||||
|
origin: "agent",
|
||||||
|
}).state;
|
||||||
|
const cancelled = transitionMobilePanel(dragging, {
|
||||||
|
type: "gesture.finish",
|
||||||
|
startedRevision: 0,
|
||||||
|
success: false,
|
||||||
|
target: "agent-list",
|
||||||
|
}).state;
|
||||||
|
|
||||||
|
expect(isMobilePanelActive(dragging, "agent")).toBe(true);
|
||||||
|
expect(isMobilePanelActive(cancelled, "agent")).toBe(true);
|
||||||
|
expect(isMobilePanelActive(cancelled, "agent-list")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the visible panel active until its closing motion settles", () => {
|
||||||
|
const initial = createMobilePanelMotionState({ target: "agent-list", revision: 0 });
|
||||||
|
const closing = transitionMobilePanel(initial, {
|
||||||
|
type: "command",
|
||||||
|
selection: { target: "agent", revision: 1 },
|
||||||
|
}).state;
|
||||||
|
|
||||||
|
expect(isMobilePanelActive(closing, "agent-list")).toBe(true);
|
||||||
|
|
||||||
|
const settled = transitionMobilePanel(closing, {
|
||||||
|
type: "animation.finished",
|
||||||
|
revision: 1,
|
||||||
|
target: "agent",
|
||||||
|
}).state;
|
||||||
|
expect(isMobilePanelActive(settled, "agent-list")).toBe(false);
|
||||||
|
expect(isMobilePanelActive(settled, "agent")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("follows programmatic commands through left, center, and right", () => {
|
it("follows programmatic commands through left, center, and right", () => {
|
||||||
const panels = new MobilePanelsScenario();
|
const panels = new MobilePanelsScenario();
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ export function isMobilePanelGestureCurrent(
|
|||||||
return state.revision === startedRevision && state.gesture?.startedRevision === startedRevision;
|
return state.revision === startedRevision && state.gesture?.startedRevision === startedRevision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMobilePanelActive(
|
||||||
|
state: MobilePanelMotionState,
|
||||||
|
panel: MobilePanelView,
|
||||||
|
): boolean {
|
||||||
|
return state.settledTarget === panel;
|
||||||
|
}
|
||||||
|
|
||||||
export function getMobilePanelFrame(position: number, width: number) {
|
export function getMobilePanelFrame(position: number, width: number) {
|
||||||
"worklet";
|
"worklet";
|
||||||
const clampedPosition = Math.max(-1, Math.min(1, position));
|
const clampedPosition = Math.max(-1, Math.min(1, position));
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { isWeb } from "@/constants/platform";
|
|||||||
import { WindowChromeRootRegion } from "@/utils/desktop-window";
|
import { WindowChromeRootRegion } from "@/utils/desktop-window";
|
||||||
import { usePanelStore, type MobilePanelView } from "@/stores/panel-store";
|
import { usePanelStore, type MobilePanelView } from "@/stores/panel-store";
|
||||||
import { getMobilePanelFrame } from "./model";
|
import { getMobilePanelFrame } from "./model";
|
||||||
import { useIsMobilePanelPresented, useMobilePanelsRuntime } from "./provider";
|
import { useIsMobilePanelActive, useMobilePanelsRuntime } from "./provider";
|
||||||
|
|
||||||
type OverlayPanel = Exclude<MobilePanelView, "agent">;
|
type OverlayPanel = Exclude<MobilePanelView, "agent">;
|
||||||
|
|
||||||
@@ -24,10 +24,8 @@ export function MobilePanelOverlay({
|
|||||||
panelStyle,
|
panelStyle,
|
||||||
}: MobilePanelOverlayProps) {
|
}: MobilePanelOverlayProps) {
|
||||||
const { position, windowWidth } = useMobilePanelsRuntime();
|
const { position, windowWidth } = useMobilePanelsRuntime();
|
||||||
const target = usePanelStore((state) => state.mobilePanel.target);
|
|
||||||
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
|
||||||
const isOpen = target === panel;
|
const isOpen = useIsMobilePanelActive(panel);
|
||||||
const isPresented = useIsMobilePanelPresented(panel);
|
|
||||||
const isLeft = panel === "agent-list";
|
const isLeft = panel === "agent-list";
|
||||||
|
|
||||||
const sidebarAnimatedStyle = useAnimatedStyle(() => {
|
const sidebarAnimatedStyle = useAnimatedStyle(() => {
|
||||||
@@ -42,10 +40,11 @@ export function MobilePanelOverlay({
|
|||||||
return { opacity: isLeft ? frame.leftBackdropOpacity : frame.rightBackdropOpacity };
|
return { opacity: isLeft ? frame.leftBackdropOpacity : frame.rightBackdropOpacity };
|
||||||
}, [isLeft, windowWidth]);
|
}, [isLeft, windowWidth]);
|
||||||
|
|
||||||
const overlayStyle = useMemo(
|
const overlayAnimatedStyle = useAnimatedStyle(() => {
|
||||||
() => [styles.overlay, { display: isPresented ? ("flex" as const) : ("none" as const) }],
|
const isVisible = isLeft ? position.value < 0 : position.value > 0;
|
||||||
[isPresented],
|
return { display: isVisible ? ("flex" as const) : ("none" as const) };
|
||||||
);
|
}, [isLeft]);
|
||||||
|
|
||||||
const positionedPanelStyle = isLeft ? styles.leftPanel : styles.rightPanel;
|
const positionedPanelStyle = isLeft ? styles.leftPanel : styles.rightPanel;
|
||||||
const backdropStyle = useMemo(
|
const backdropStyle = useMemo(
|
||||||
() => [styles.backdrop, backdropAnimatedStyle],
|
() => [styles.backdrop, backdropAnimatedStyle],
|
||||||
@@ -73,19 +72,25 @@ export function MobilePanelOverlay({
|
|||||||
{/* Fabric needs an always-mounted native host to attach the close handler while the
|
{/* Fabric needs an always-mounted native host to attach the close handler while the
|
||||||
retained panel content is hidden. nativeID keeps that host registered. */}
|
retained panel content is hidden. nativeID keeps that host registered. */}
|
||||||
<View
|
<View
|
||||||
|
accessibilityElementsHidden={!isOpen}
|
||||||
collapsable={false}
|
collapsable={false}
|
||||||
|
importantForAccessibility={isOpen ? "auto" : "no-hide-descendants"}
|
||||||
nativeID={`${panel}-gesture-host`}
|
nativeID={`${panel}-gesture-host`}
|
||||||
pointerEvents={overlayPointerEvents}
|
pointerEvents={overlayPointerEvents}
|
||||||
style={styles.overlay}
|
style={styles.overlay}
|
||||||
>
|
>
|
||||||
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
<Animated.View
|
||||||
|
pointerEvents={overlayPointerEvents}
|
||||||
|
style={[styles.overlay, overlayAnimatedStyle]}
|
||||||
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
|
accessible={false}
|
||||||
accessibilityElementsHidden
|
accessibilityElementsHidden
|
||||||
importantForAccessibility="no-hide-descendants"
|
importantForAccessibility="no-hide-descendants"
|
||||||
onPress={showMobileAgent}
|
onPress={showMobileAgent}
|
||||||
pointerEvents={isOpen ? "auto" : "none"}
|
pointerEvents={isOpen ? "auto" : "none"}
|
||||||
style={StyleSheet.absoluteFillObject}
|
style={StyleSheet.absoluteFillObject}
|
||||||
testID={`${panel}-backdrop`}
|
testID={isOpen ? `${panel}-backdrop` : undefined}
|
||||||
>
|
>
|
||||||
<Animated.View pointerEvents="none" style={backdropStyle} />
|
<Animated.View pointerEvents="none" style={backdropStyle} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -93,7 +98,7 @@ export function MobilePanelOverlay({
|
|||||||
<Animated.View pointerEvents={isOpen ? "auto" : "none"} style={combinedPanelStyle}>
|
<Animated.View pointerEvents={isOpen ? "auto" : "none"} style={combinedPanelStyle}>
|
||||||
<WindowChromeRootRegion corners="both">{children}</WindowChromeRootRegion>
|
<WindowChromeRootRegion corners="both">{children}</WindowChromeRootRegion>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,18 +39,6 @@ import {
|
|||||||
|
|
||||||
const ANIMATION_DURATION = 220;
|
const ANIMATION_DURATION = 220;
|
||||||
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
||||||
const LEFT_PANEL_MASK = 1;
|
|
||||||
const RIGHT_PANEL_MASK = 2;
|
|
||||||
|
|
||||||
function getPanelMask(panel: MobilePanelView): number {
|
|
||||||
if (panel === "agent-list") {
|
|
||||||
return LEFT_PANEL_MASK;
|
|
||||||
}
|
|
||||||
if (panel === "file-explorer") {
|
|
||||||
return RIGHT_PANEL_MASK;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MobilePanelsRuntime {
|
interface MobilePanelsRuntime {
|
||||||
beginGesture: (input: BeginGestureInput) => number;
|
beginGesture: (input: BeginGestureInput) => number;
|
||||||
@@ -69,7 +57,6 @@ interface MobilePanelsRuntime {
|
|||||||
|
|
||||||
interface BeginGestureInput {
|
interface BeginGestureInput {
|
||||||
origin: MobilePanelView;
|
origin: MobilePanelView;
|
||||||
preview: MobilePanelView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FinishGestureInput {
|
interface FinishGestureInput {
|
||||||
@@ -79,7 +66,7 @@ interface FinishGestureInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MobilePanelsContext = createContext<MobilePanelsRuntime | null>(null);
|
const MobilePanelsContext = createContext<MobilePanelsRuntime | null>(null);
|
||||||
const MobilePanelPresentationContext = createContext(0);
|
const MobilePanelActiveContext = createContext<MobilePanelView>("agent");
|
||||||
|
|
||||||
export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
||||||
const { width: windowWidth } = useWindowDimensions();
|
const { width: windowWidth } = useWindowDimensions();
|
||||||
@@ -92,7 +79,7 @@ export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
|||||||
const leftCloseGestureRef = useRef<GestureType | undefined>(undefined);
|
const leftCloseGestureRef = useRef<GestureType | undefined>(undefined);
|
||||||
const rightOpenGestureRef = useRef<GestureType | undefined>(undefined);
|
const rightOpenGestureRef = useRef<GestureType | undefined>(undefined);
|
||||||
const rightCloseGestureRef = useRef<GestureType | undefined>(undefined);
|
const rightCloseGestureRef = useRef<GestureType | undefined>(undefined);
|
||||||
const [presentedPanels, setPresentedPanels] = useState(getPanelMask(initialSelection.target));
|
const [activePanel, setActivePanel] = useState(initialSelection.target);
|
||||||
|
|
||||||
const setOpenGestureBlocked = useCallback(
|
const setOpenGestureBlocked = useCallback(
|
||||||
(owner: symbol, blocked: boolean) => {
|
(owner: symbol, blocked: boolean) => {
|
||||||
@@ -106,19 +93,15 @@ export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
|||||||
[openGesturesBlocked],
|
[openGesturesBlocked],
|
||||||
);
|
);
|
||||||
|
|
||||||
const presentPanel = useCallback((panel: MobilePanelView) => {
|
const publishActivePanel = useCallback((panel: MobilePanelView, revision: number) => {
|
||||||
const mask = getPanelMask(panel);
|
|
||||||
if (mask) {
|
|
||||||
setPresentedPanels((current) => current | mask);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const settlePresentation = useCallback((panel: MobilePanelView, revision: number) => {
|
|
||||||
const selection = usePanelStore.getState().mobilePanel;
|
const selection = usePanelStore.getState().mobilePanel;
|
||||||
if (selection.revision !== revision || selection.target !== panel) {
|
if (selection.revision !== revision || selection.target !== panel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPresentedPanels(getPanelMask(panel));
|
if (isNative && panel !== "agent") {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
}
|
||||||
|
setActivePanel(panel);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const animateTransition = useCallback(
|
const animateTransition = useCallback(
|
||||||
@@ -146,11 +129,11 @@ export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
motionState.value = settled.state;
|
motionState.value = settled.state;
|
||||||
scheduleOnRN(settlePresentation, target, revision);
|
scheduleOnRN(publishActivePanel, target, revision);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[motionState, position, settlePresentation],
|
[motionState, position, publishActivePanel],
|
||||||
);
|
);
|
||||||
|
|
||||||
const applySelection = useCallback(
|
const applySelection = useCallback(
|
||||||
@@ -176,18 +159,12 @@ export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
|||||||
if (selection === previousState.mobilePanel) {
|
if (selection === previousState.mobilePanel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selection.target !== "agent") {
|
|
||||||
presentPanel(selection.target);
|
|
||||||
if (isNative) {
|
|
||||||
Keyboard.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scheduleOnUI(applySelection, selection);
|
scheduleOnUI(applySelection, selection);
|
||||||
});
|
});
|
||||||
}, [applySelection, presentPanel]);
|
}, [applySelection]);
|
||||||
|
|
||||||
const beginGesture = useCallback(
|
const beginGesture = useCallback(
|
||||||
({ origin, preview }: BeginGestureInput): number => {
|
({ origin }: BeginGestureInput): number => {
|
||||||
"worklet";
|
"worklet";
|
||||||
const currentState = motionState.value;
|
const currentState = motionState.value;
|
||||||
if (!canBeginMobilePanelGesture(currentState, origin, position.value)) {
|
if (!canBeginMobilePanelGesture(currentState, origin, position.value)) {
|
||||||
@@ -199,10 +176,9 @@ export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
|||||||
});
|
});
|
||||||
motionState.value = transition.state;
|
motionState.value = transition.state;
|
||||||
cancelAnimation(position);
|
cancelAnimation(position);
|
||||||
scheduleOnRN(presentPanel, preview);
|
|
||||||
return transition.state.gesture?.startedRevision ?? -1;
|
return transition.state.gesture?.startedRevision ?? -1;
|
||||||
},
|
},
|
||||||
[motionState, position, presentPanel],
|
[motionState, position],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateGesture = useCallback(
|
const updateGesture = useCallback(
|
||||||
@@ -266,9 +242,9 @@ export function MobilePanelsProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MobilePanelsContext.Provider value={value}>
|
<MobilePanelsContext.Provider value={value}>
|
||||||
<MobilePanelPresentationContext.Provider value={presentedPanels}>
|
<MobilePanelActiveContext.Provider value={activePanel}>
|
||||||
{children}
|
{children}
|
||||||
</MobilePanelPresentationContext.Provider>
|
</MobilePanelActiveContext.Provider>
|
||||||
</MobilePanelsContext.Provider>
|
</MobilePanelsContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -282,9 +258,8 @@ export function useMobilePanelsRuntime(): MobilePanelsRuntime {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsMobilePanelPresented(panel: MobilePanelView): boolean {
|
export function useIsMobilePanelActive(panel: MobilePanelView): boolean {
|
||||||
const presentedPanels = useContext(MobilePanelPresentationContext);
|
return useContext(MobilePanelActiveContext) === panel;
|
||||||
return (presentedPanels & getPanelMask(panel)) !== 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBlockMobilePanelOpenGestures(blocked: boolean): void {
|
export function useBlockMobilePanelOpenGestures(blocked: boolean): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user