2026-04-24 06:10:18 +08:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import type { ReactNode } from "react";
|
|
|
|
|
import {
|
|
|
|
|
TreeShellIframeHost,
|
|
|
|
|
type TreeShellPickerItem,
|
|
|
|
|
} from "@/components/sidebar/tree-shell-iframe-host";
|
2026-04-26 04:29:23 +08:00
|
|
|
import type { KernelFileTreeProjectionItem } from "@/lib/kernel-file-tree";
|
|
|
|
|
import type { PageTreeProjectionItem } from "@/lib/tree-projection";
|
2026-04-24 06:10:18 +08:00
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
export type TreeRendererFamily = "react" | "rust_family";
|
|
|
|
|
|
|
|
|
|
export type TreeShellHostMode = "page" | "filetree" | "picker";
|
|
|
|
|
|
2026-04-26 19:35:52 +08:00
|
|
|
const RUST_RENDERER_CONTRACT = "rust_renderer_input_v1";
|
|
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
export type TreeShellPickerCommand = {
|
|
|
|
|
kind: "next" | "previous" | "home" | "end" | "pick";
|
|
|
|
|
seq: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type FileTreeShellInternalDropPayload = {
|
|
|
|
|
targetDocumentId: string | null;
|
|
|
|
|
targetRowId: string | null;
|
|
|
|
|
targetRowKind: string | null;
|
|
|
|
|
targetAssetId: string | null;
|
|
|
|
|
rowIds: string[];
|
|
|
|
|
copy: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type FileTreeShellExternalDropPayload = {
|
|
|
|
|
targetDocumentId: string | null;
|
|
|
|
|
targetRowId: string | null;
|
|
|
|
|
targetRowKind: string | null;
|
|
|
|
|
targetAssetId: string | null;
|
|
|
|
|
files: FileList | File[];
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-24 06:10:18 +08:00
|
|
|
type TreeShellHostProps = {
|
|
|
|
|
mode: TreeShellHostMode;
|
|
|
|
|
surfaceTestId: string;
|
|
|
|
|
rendererFamily?: TreeRendererFamily;
|
|
|
|
|
className?: string;
|
|
|
|
|
treeShellEnabled?: boolean;
|
2026-04-26 04:29:23 +08:00
|
|
|
fallbackImplementation?: string;
|
2026-04-24 06:10:18 +08:00
|
|
|
workspaceId?: string | null;
|
|
|
|
|
rootNodeId?: string | null;
|
|
|
|
|
activeDocumentId?: string | null;
|
2026-04-26 04:29:23 +08:00
|
|
|
focusedDocumentId?: string | null;
|
|
|
|
|
activePickerItemKey?: string | null;
|
2026-04-24 06:10:18 +08:00
|
|
|
allowRootPick?: boolean;
|
|
|
|
|
excludeIds?: string[];
|
2026-04-26 04:29:23 +08:00
|
|
|
pickerCommand?: TreeShellPickerCommand | null;
|
2026-04-24 06:10:18 +08:00
|
|
|
pickerItems?: TreeShellPickerItem[];
|
2026-04-26 04:29:23 +08:00
|
|
|
pageTreeItems?: PageTreeProjectionItem[];
|
|
|
|
|
inlineFileTreeItems?: KernelFileTreeProjectionItem[];
|
2026-04-24 06:10:18 +08:00
|
|
|
channel?: string;
|
|
|
|
|
host?: string;
|
|
|
|
|
onNavigate?: (documentId: string) => void;
|
|
|
|
|
onPick?: (targetId: string | null) => void;
|
2026-04-26 04:29:23 +08:00
|
|
|
onPickerFocusChange?: (payload: { itemKey: string | null; documentId: string | null }) => void;
|
2026-04-24 06:10:18 +08:00
|
|
|
onPageContextMenu?: (payload: { documentId: string; x: number; y: number }) => void;
|
2026-04-26 04:29:23 +08:00
|
|
|
onPageExpandChange?: (payload: { documentId: string | null; expanded: boolean }) => void;
|
|
|
|
|
onPageFocusChange?: (payload: { documentId: string | null }) => void;
|
2026-04-24 06:10:18 +08:00
|
|
|
onFileTreeContextMenu?: (payload: {
|
|
|
|
|
documentId: string | null;
|
|
|
|
|
assetId: string | null;
|
|
|
|
|
rowId: string | null;
|
|
|
|
|
rowKind: string | null;
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
}) => void;
|
|
|
|
|
onFileTreeSelectionChange?: (payload: {
|
|
|
|
|
selectedRowIds: string[];
|
|
|
|
|
anchorRowId: string | null;
|
|
|
|
|
focusedRowId: string | null;
|
|
|
|
|
}) => void;
|
2026-04-26 04:29:23 +08:00
|
|
|
onInternalDrop?: (payload: FileTreeShellInternalDropPayload) => void;
|
|
|
|
|
onDropFiles?: (payload: FileTreeShellExternalDropPayload) => void;
|
2026-04-24 06:10:18 +08:00
|
|
|
onAssetOpen?: (payload: { assetId: string; documentId: string | null }) => void;
|
|
|
|
|
onTreeMutation?: (payload: { type: string; documentId: string | null }) => void;
|
|
|
|
|
children: ReactNode;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function TreeShellHost({
|
|
|
|
|
mode,
|
|
|
|
|
surfaceTestId,
|
|
|
|
|
rendererFamily = "react",
|
|
|
|
|
className,
|
|
|
|
|
treeShellEnabled = true,
|
2026-04-26 04:29:23 +08:00
|
|
|
fallbackImplementation,
|
2026-04-24 06:10:18 +08:00
|
|
|
workspaceId = null,
|
|
|
|
|
rootNodeId = null,
|
|
|
|
|
activeDocumentId = null,
|
2026-04-26 04:29:23 +08:00
|
|
|
focusedDocumentId = null,
|
|
|
|
|
activePickerItemKey = null,
|
2026-04-24 06:10:18 +08:00
|
|
|
allowRootPick = false,
|
|
|
|
|
excludeIds = [],
|
2026-04-26 04:29:23 +08:00
|
|
|
pickerCommand = null,
|
|
|
|
|
pickerItems,
|
|
|
|
|
pageTreeItems,
|
|
|
|
|
inlineFileTreeItems,
|
2026-04-24 06:10:18 +08:00
|
|
|
channel,
|
|
|
|
|
host,
|
|
|
|
|
onNavigate,
|
|
|
|
|
onPick,
|
2026-04-26 04:29:23 +08:00
|
|
|
onPickerFocusChange,
|
2026-04-24 06:10:18 +08:00
|
|
|
onPageContextMenu,
|
2026-04-26 04:29:23 +08:00
|
|
|
onPageExpandChange,
|
|
|
|
|
onPageFocusChange,
|
2026-04-24 06:10:18 +08:00
|
|
|
onFileTreeContextMenu,
|
|
|
|
|
onFileTreeSelectionChange,
|
|
|
|
|
onInternalDrop,
|
|
|
|
|
onDropFiles,
|
|
|
|
|
onAssetOpen,
|
|
|
|
|
onTreeMutation,
|
|
|
|
|
children,
|
|
|
|
|
}: TreeShellHostProps) {
|
|
|
|
|
const useRustHost = rendererFamily === "rust_family";
|
2026-04-26 04:29:23 +08:00
|
|
|
const useIframeHost = useRustHost && Boolean(workspaceId?.trim());
|
2026-04-24 06:10:18 +08:00
|
|
|
const hostKind = rendererFamily === "rust_family" ? "rust_family" : "react";
|
2026-04-26 19:35:52 +08:00
|
|
|
const rendererContract = useRustHost ? RUST_RENDERER_CONTRACT : undefined;
|
2026-04-24 06:10:18 +08:00
|
|
|
const implementation = useIframeHost
|
2026-04-26 19:35:52 +08:00
|
|
|
? "rust_inline_compat_host"
|
2026-04-26 04:29:23 +08:00
|
|
|
: fallbackImplementation ??
|
|
|
|
|
(rendererFamily === "rust_family" ? "react_fallback" : "react_primary");
|
2026-04-24 06:10:18 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
data-testid={surfaceTestId}
|
|
|
|
|
data-shell-mode={mode}
|
|
|
|
|
data-renderer-family={rendererFamily}
|
|
|
|
|
data-tree-host-kind={hostKind}
|
|
|
|
|
data-tree-host-implementation={implementation}
|
2026-04-26 19:35:52 +08:00
|
|
|
data-tree-renderer-contract={rendererContract}
|
2026-04-26 04:29:23 +08:00
|
|
|
data-page-tree-focused-id={mode === "page" ? (focusedDocumentId ?? "") : undefined}
|
2026-04-24 06:10:18 +08:00
|
|
|
className={cn(className)}
|
|
|
|
|
>
|
|
|
|
|
{useRustHost ? (
|
|
|
|
|
<div
|
|
|
|
|
data-testid={`${surfaceTestId}-rust-host`}
|
|
|
|
|
data-tree-host-mode={mode}
|
|
|
|
|
data-tree-host-kind="rust_family"
|
|
|
|
|
data-tree-host-implementation={implementation}
|
2026-04-26 19:35:52 +08:00
|
|
|
data-tree-renderer-contract={rendererContract}
|
2026-04-24 06:10:18 +08:00
|
|
|
className="contents"
|
|
|
|
|
>
|
|
|
|
|
{useIframeHost && workspaceId ? (
|
|
|
|
|
<TreeShellIframeHost
|
|
|
|
|
mode={mode}
|
|
|
|
|
surfaceTestId={surfaceTestId}
|
|
|
|
|
workspaceId={workspaceId}
|
|
|
|
|
rootNodeId={rootNodeId}
|
|
|
|
|
activeDocumentId={activeDocumentId}
|
2026-04-26 04:29:23 +08:00
|
|
|
focusedDocumentId={focusedDocumentId}
|
|
|
|
|
activePickerItemKey={activePickerItemKey}
|
2026-04-24 06:10:18 +08:00
|
|
|
allowRootPick={allowRootPick}
|
|
|
|
|
excludeIds={excludeIds}
|
2026-04-26 04:29:23 +08:00
|
|
|
pickerCommand={pickerCommand}
|
2026-04-24 06:10:18 +08:00
|
|
|
pickerItems={pickerItems}
|
2026-04-26 04:29:23 +08:00
|
|
|
pageTreeItems={pageTreeItems}
|
|
|
|
|
inlineFileTreeItems={inlineFileTreeItems}
|
2026-04-24 06:10:18 +08:00
|
|
|
channel={channel}
|
|
|
|
|
host={host}
|
|
|
|
|
onNavigate={onNavigate}
|
|
|
|
|
onPick={onPick}
|
2026-04-26 04:29:23 +08:00
|
|
|
onPickerFocusChange={onPickerFocusChange}
|
2026-04-24 06:10:18 +08:00
|
|
|
onPageContextMenu={onPageContextMenu}
|
2026-04-26 04:29:23 +08:00
|
|
|
onPageExpandChange={onPageExpandChange}
|
|
|
|
|
onPageFocusChange={onPageFocusChange}
|
2026-04-24 06:10:18 +08:00
|
|
|
onFileTreeContextMenu={onFileTreeContextMenu}
|
|
|
|
|
onFileTreeSelectionChange={onFileTreeSelectionChange}
|
|
|
|
|
onInternalDrop={onInternalDrop}
|
|
|
|
|
onDropFiles={onDropFiles}
|
|
|
|
|
onAssetOpen={onAssetOpen}
|
|
|
|
|
onTreeMutation={onTreeMutation}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
children
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
children
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|