feat: init
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
chuan
2025-11-11 01:56:44 +08:00
commit bba4bb40c8
4638 changed files with 447437 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
// plane web imports
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
export * from "./root";
export * from "./types";
export const PAGE_NAVIGATION_PANE_WIDTH = 294;
export const PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM = "paneTab";
export const PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM = "version";
export const PAGE_NAVIGATION_PANE_TAB_KEYS = ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => tab.key);

View File

@@ -0,0 +1,117 @@
import React, { useCallback } from "react";
import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation";
import { ArrowRightCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
// hooks
import { useQueryParams } from "@/hooks/use-query-params";
// plane web components
import type { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane";
// store
import type { EPageStoreType } from "@/plane-web/hooks/store";
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import type { TPageRootHandlers } from "../editor/page-root";
import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root";
import { PageNavigationPaneTabsList } from "./tabs-list";
import type { INavigationPaneExtension } from "./types/extensions";
import {
PAGE_NAVIGATION_PANE_TAB_KEYS,
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
PAGE_NAVIGATION_PANE_WIDTH,
} from "./index";
type Props = {
handleClose: () => void;
isNavigationPaneOpen: boolean;
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
// Generic extension system for additional navigation pane content
extensions?: INavigationPaneExtension[];
storeType: EPageStoreType;
};
export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
const { handleClose, isNavigationPaneOpen, page, versionHistory, extensions = [], storeType } = props;
// navigation
const router = useRouter();
const searchParams = useSearchParams();
// query params
const { updateQueryParams } = useQueryParams();
// derived values
const navigationPaneQueryParam = searchParams.get(
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
) as TPageNavigationPaneTab | null;
const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline";
const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab);
// Check if any extension is currently active based on query parameters
const ActiveExtension = extensions.find((extension) => {
const paneTabValue = searchParams.get(PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM);
const hasVersionParam = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM);
// Extension is active ONLY when paneTab matches AND no regular navigation params are present
return paneTabValue === extension.triggerParam && !hasVersionParam;
});
// Don't show tabs when an extension is active
const showNavigationTabs = !ActiveExtension && isNavigationPaneOpen;
// Use extension width if available, otherwise fall back to default
const paneWidth = ActiveExtension?.width ?? PAGE_NAVIGATION_PANE_WIDTH;
// translation
const { t } = useTranslation();
const handleTabChange = useCallback(
(index: number) => {
const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index];
const isUpdatedTabInfo = updatedTab === "info";
const updatedRoute = updateQueryParams({
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab },
paramsToRemove: !isUpdatedTabInfo ? [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM] : undefined,
});
router.push(updatedRoute);
},
[router, updateQueryParams]
);
return (
<aside
className="flex-shrink-0 h-full flex flex-col bg-custom-background-100 pt-3.5 border-l border-custom-border-200 transition-all duration-300 ease-out"
style={{
width: `${paneWidth}px`,
marginRight: isNavigationPaneOpen ? "0px" : `-${paneWidth}px`,
}}
>
<div className="mb-3.5 px-3.5">
<Tooltip tooltipContent={t("page_navigation_pane.close_button")}>
<button
type="button"
className="size-3.5 grid place-items-center text-custom-text-200 hover:text-custom-text-100 transition-colors"
onClick={handleClose}
aria-label={t("page_navigation_pane.close_button")}
>
<ArrowRightCircle className="size-3.5" />
</button>
</Tooltip>
</div>
<div className="flex-1 flex flex-col overflow-hidden animate-slide-in-right">
{ActiveExtension ? (
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
) : showNavigationTabs ? (
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}>
<PageNavigationPaneTabsList />
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
</Tab.Group>
) : null}
</div>
</aside>
);
});

View File

@@ -0,0 +1,120 @@
import { useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { Download } from "lucide-react";
// plane imports
import { CORE_EXTENSIONS } from "@plane/editor";
import type { TEditorAsset } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@plane/utils";
// plane web imports
import { AdditionalPageNavigationPaneAssetItem } from "@/plane-web/components/pages/navigation-pane/tab-panels/assets";
import { PageNavigationPaneAssetsTabEmptyState } from "@/plane-web/components/pages/navigation-pane/tab-panels/empty-states/assets";
// store
import type { TPageInstance } from "@/store/pages/base-page";
type Props = {
page: TPageInstance;
};
type AssetItemProps = {
asset: TEditorAsset;
page: TPageInstance;
};
const AssetItem = observer((props: AssetItemProps) => {
const { asset, page } = props;
// navigation
const { workspaceSlug } = useParams();
// derived values
const { project_ids } = page;
// translation
const { t } = useTranslation();
const assetSrc: string = useMemo(() => {
if (!asset.src || !workspaceSlug) return "";
if (asset.src.startsWith("http")) {
return asset.src;
} else {
return (
getEditorAssetSrc({
assetId: asset.src,
projectId: project_ids?.[0],
workspaceSlug: workspaceSlug.toString(),
}) ?? ""
);
}
}, [asset.src, project_ids, workspaceSlug]);
const assetDownloadSrc: string = useMemo(() => {
if (!asset.src || !workspaceSlug) return "";
if (asset.src.startsWith("http")) {
return asset.src;
} else {
return (
getEditorAssetDownloadSrc({
assetId: asset.src,
projectId: project_ids?.[0],
workspaceSlug: workspaceSlug.toString(),
}) ?? ""
);
}
}, [asset.src, project_ids, workspaceSlug]);
if ([CORE_EXTENSIONS.IMAGE, CORE_EXTENSIONS.CUSTOM_IMAGE].includes(asset.type as CORE_EXTENSIONS))
return (
<a
href={asset.href}
className="relative group/asset-item h-12 flex items-center gap-2 pr-2 rounded border border-custom-border-200 hover:bg-custom-background-80 transition-colors"
>
<div
className="flex-shrink-0 w-11 h-12 rounded-l bg-cover bg-no-repeat bg-center"
style={{
backgroundImage: `url('${assetSrc}')`,
}}
/>
<div className="flex-1 space-y-0.5 truncate">
<p className="text-sm font-medium truncate">{asset.name}</p>
<div className="flex items-end justify-between gap-2">
<p className="shrink-0 text-xs text-custom-text-200" />
<a
href={assetDownloadSrc}
target="_blank"
rel="noreferrer noopener"
className="shrink-0 py-0.5 px-1 flex items-center gap-1 rounded text-custom-text-200 hover:text-custom-text-100 opacity-0 pointer-events-none group-hover/asset-item:opacity-100 group-hover/asset-item:pointer-events-auto transition-opacity"
>
<Download className="shrink-0 size-3" />
<span className="text-xs font-medium">{t("page_navigation_pane.tabs.assets.download_button")}</span>
</a>
</div>
</div>
</a>
);
return (
<AdditionalPageNavigationPaneAssetItem
asset={asset}
assetSrc={assetSrc}
assetDownloadSrc={assetDownloadSrc}
page={page}
/>
);
});
export const PageNavigationPaneAssetsTabPanel: React.FC<Props> = observer((props) => {
const { page } = props;
// derived values
const {
editor: { assetsList },
} = page;
if (assetsList.length === 0) return <PageNavigationPaneAssetsTabEmptyState />;
return (
<div className="mt-5 space-y-4">
{assetsList?.map((asset) => (
<AssetItem key={asset.id} asset={asset} page={page} />
))}
</div>
);
});

View File

@@ -0,0 +1,68 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Avatar } from "@plane/ui";
import { calculateTimeAgoShort, getFileURL, renderFormattedDate } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
// store
import type { TPageInstance } from "@/store/pages/base-page";
type Props = {
page: TPageInstance;
};
export const PageNavigationPaneInfoTabActorsInfo: React.FC<Props> = observer((props) => {
const { page } = props;
// navigation
const { workspaceSlug } = useParams();
// store hooks
const { getUserDetails } = useMember();
// derived values
const { owned_by, updated_by } = page;
const editorInformation = updated_by ? getUserDetails(updated_by) : undefined;
const creatorInformation = owned_by ? getUserDetails(owned_by) : undefined;
// translation
const { t } = useTranslation();
return (
<div className="space-y-3 mt-4">
<div>
<p className="text-xs font-medium text-custom-text-300">
{t("page_navigation_pane.tabs.info.actors_info.edited_by")}
</p>
<div className="mt-2 flex items-center justify-between gap-2 text-sm font-medium">
<Link href={`/${workspaceSlug?.toString()}/profile/${page.updated_by}`} className="flex items-center gap-1">
<Avatar
src={getFileURL(editorInformation?.avatar_url ?? "")}
name={editorInformation?.display_name}
className="flex-shrink-0"
size="sm"
/>
<span>{editorInformation?.display_name ?? t("common.deactivated_user")}</span>
</Link>
<span className="flex-shrink-0 text-custom-text-300">{calculateTimeAgoShort(page.updated_at ?? "")} ago</span>
</div>
</div>
<div>
<p className="text-xs font-medium text-custom-text-300">
{t("page_navigation_pane.tabs.info.actors_info.created_by")}
</p>
<div className="mt-2 flex items-center justify-between gap-2 text-sm font-medium">
<Link href={`/${workspaceSlug?.toString()}/profile/${page.created_by}`} className="flex items-center gap-1">
<Avatar
src={getFileURL(creatorInformation?.avatar_url ?? "")}
name={creatorInformation?.display_name}
className="flex-shrink-0"
size="sm"
/>
<span>{creatorInformation?.display_name ?? t("common.deactivated_user")}</span>
</Link>
<span className="flex-shrink-0 text-custom-text-300">{renderFormattedDate(page.created_at)}</span>
</div>
</div>
</div>
);
});

View File

@@ -0,0 +1,82 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import type { TDocumentInfo } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { getReadTimeFromWordsCount } from "@plane/utils";
// store
import type { TPageInstance } from "@/store/pages/base-page";
type Props = {
page: TPageInstance;
};
const DEFAULT_DOCUMENT_INFO: TDocumentInfo = {
words: 0,
characters: 0,
paragraphs: 0,
};
export const PageNavigationPaneInfoTabDocumentInfo: React.FC<Props> = observer((props) => {
const { page } = props;
// states
const [documentInfo, setDocumentInfo] = useState<TDocumentInfo>(DEFAULT_DOCUMENT_INFO);
// derived values
const {
editor: { editorRef },
} = page;
// translation
const { t } = useTranslation();
// subscribe to asset changes
useEffect(() => {
const unsubscribe = editorRef?.onDocumentInfoChange(setDocumentInfo);
// for initial render of this component to get the editor assets
setDocumentInfo(editorRef?.getDocumentInfo() ?? DEFAULT_DOCUMENT_INFO);
return () => {
unsubscribe?.();
};
}, [editorRef]);
const secondsToReadableTime = useCallback(() => {
const wordsCount = documentInfo.words;
const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0));
return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`;
}, [documentInfo.words]);
const documentInfoCards = useMemo(
() => [
{
key: "words-count",
title: t("page_navigation_pane.tabs.info.document_info.words"),
info: documentInfo.words,
},
{
key: "characters-count",
title: t("page_navigation_pane.tabs.info.document_info.characters"),
info: documentInfo.characters,
},
{
key: "paragraphs-count",
title: t("page_navigation_pane.tabs.info.document_info.paragraphs"),
info: documentInfo.paragraphs,
},
{
key: "read-time",
title: t("page_navigation_pane.tabs.info.document_info.read_time"),
info: secondsToReadableTime(),
},
],
[documentInfo, secondsToReadableTime, t]
);
return (
<div className="grid grid-cols-2 gap-2">
{documentInfoCards.map((card) => (
<div key={card.key} className="p-2 bg-custom-background-90 rounded">
<h6 className="text-base font-semibold">{card.info}</h6>
<p className="mt-1.5 text-sm text-custom-text-300 font-medium">{card.title}</p>
</div>
))}
</div>
);
});

View File

@@ -0,0 +1,27 @@
import { observer } from "mobx-react";
// components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageNavigationPaneInfoTabActorsInfo } from "./actors-info";
import { PageNavigationPaneInfoTabDocumentInfo } from "./document-info";
import { PageNavigationPaneInfoTabVersionHistory } from "./version-history";
type Props = {
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
};
export const PageNavigationPaneInfoTabPanel: React.FC<Props> = observer((props) => {
const { page, versionHistory } = props;
return (
<div className="mt-5">
<PageNavigationPaneInfoTabDocumentInfo page={page} />
<PageNavigationPaneInfoTabActorsInfo page={page} />
<div className="flex-shrink-0 h-px bg-custom-background-80 my-3" />
<PageNavigationPaneInfoTabVersionHistory page={page} versionHistory={versionHistory} />
</div>
);
});

View File

@@ -0,0 +1,142 @@
import { useCallback } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import useSWR from "swr";
// plane imports
import { useTranslation } from "@plane/i18n";
import type { TPageVersion } from "@plane/types";
import { Avatar } from "@plane/ui";
import { cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils";
// components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useQueryParams } from "@/hooks/use-query-params";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM } from "../..";
type Props = {
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
};
type VersionHistoryItemProps = {
getVersionLink: (versionID: string) => string;
isVersionActive: boolean;
version: TPageVersion;
};
const VersionHistoryItem = observer((props: VersionHistoryItemProps) => {
const { getVersionLink, isVersionActive, version } = props;
// store hooks
const { getUserDetails } = useMember();
// derived values
const versionCreator = getUserDetails(version.owned_by);
// translation
const { t } = useTranslation();
return (
<li className="relative flex items-center gap-x-4 text-xs font-medium">
{/* timeline icon */}
<div className="relative size-6 flex-none grid place-items-center">
<div className="size-2 rounded-full bg-custom-background-80" />
</div>
{/* end timeline icon */}
<Link
href={getVersionLink(version.id)}
className={cn("block flex-1 hover:bg-custom-background-90 rounded-md py-2 px-1", {
"bg-custom-background-80 hover:bg-custom-background-80": isVersionActive,
})}
>
<p className="text-custom-text-300">
{renderFormattedDate(version.last_saved_at)}, {renderFormattedTime(version.last_saved_at)}
</p>
<p className="mt-1 flex items-center gap-1">
<Avatar
size="sm"
src={getFileURL(versionCreator?.avatar_url ?? "")}
name={versionCreator?.display_name}
className="flex-shrink-0"
/>
<span>{versionCreator?.display_name ?? t("common.deactivated_user")}</span>
</p>
</Link>
</li>
);
});
export const PageNavigationPaneInfoTabVersionHistory: React.FC<Props> = observer((props) => {
const { page, versionHistory } = props;
// navigation
const searchParams = useSearchParams();
const activeVersion = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM);
// derived values
const { id } = page;
// translation
const { t } = useTranslation();
// query params
const { updateQueryParams } = useQueryParams();
// fetch all versions
const { data: versionsList } = useSWR(
id ? `PAGE_VERSIONS_LIST_${id}` : null,
id ? () => versionHistory.fetchAllVersions(id) : null
);
const getVersionLink = useCallback(
(versionID?: string) => {
if (versionID) {
return updateQueryParams({
paramsToAdd: { [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM]: versionID },
});
} else {
return updateQueryParams({
paramsToRemove: [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM],
});
}
},
[updateQueryParams]
);
return (
<div>
<p className="text-xs font-medium text-custom-text-200">
{t("page_navigation_pane.tabs.info.version_history.label")}
</p>
<div className="mt-3">
<ul role="list" className="relative">
{/* timeline line */}
<div className={cn("absolute left-0 top-0 h-full flex w-6 justify-center")}>
<div className="w-px bg-custom-background-80" />
</div>
{/* end timeline line */}
<li className="relative flex items-center gap-x-4 text-xs font-medium">
{/* timeline icon */}
<div className="relative size-6 flex-none rounded-full grid place-items-center bg-custom-primary-100/20">
<div className="size-2.5 rounded-full bg-custom-primary-100/40" />
</div>
{/* end timeline icon */}
<Link
href={getVersionLink()}
className={cn("flex-1 hover:bg-custom-background-90 rounded-md py-2 px-1", {
"bg-custom-background-80 hover:bg-custom-background-80": !activeVersion,
})}
>
{t("page_navigation_pane.tabs.info.version_history.current_version")}
</Link>
</li>
{versionsList?.map((version) => (
<VersionHistoryItem
key={version.id}
getVersionLink={getVersionLink}
isVersionActive={activeVersion === version.id}
version={version}
/>
))}
</ul>
</div>
</div>
);
});

View File

@@ -0,0 +1,28 @@
// plane web imports
import { PageNavigationPaneOutlineTabEmptyState } from "@/plane-web/components/pages/navigation-pane/tab-panels/empty-states/outline";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageContentBrowser } from "../../editor/summary";
type Props = {
page: TPageInstance;
};
export const PageNavigationPaneOutlineTabPanel: React.FC<Props> = (props) => {
const { page } = props;
// derived values
const {
editor: { editorRef },
} = page;
return (
<div className="size-full pt-3 space-y-1">
<PageContentBrowser
className="mt-0"
editorRef={editorRef}
emptyState={<PageNavigationPaneOutlineTabEmptyState />}
/>
</div>
);
};

View File

@@ -0,0 +1,39 @@
import React from "react";
import { Tab } from "@headlessui/react";
// components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// plane web imports
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
import { PageNavigationPaneAdditionalTabPanelsRoot } from "@/plane-web/components/pages/navigation-pane/tab-panels/root";
// store
import type { TPageInstance } from "@/store/pages/base-page";
// local imports
import { PageNavigationPaneAssetsTabPanel } from "./assets";
import { PageNavigationPaneInfoTabPanel } from "./info/root";
import { PageNavigationPaneOutlineTabPanel } from "./outline";
type Props = {
page: TPageInstance;
versionHistory: Pick<TPageRootHandlers, "fetchAllVersions" | "fetchVersionDetails">;
};
export const PageNavigationPaneTabPanelsRoot: React.FC<Props> = (props) => {
const { page, versionHistory } = props;
return (
<Tab.Panels as={React.Fragment}>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab.Panel
key={tab.key}
as="div"
className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none"
>
{tab.key === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />}
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
<PageNavigationPaneAdditionalTabPanelsRoot activeTab={tab.key} page={page} />
</Tab.Panel>
))}
</Tab.Panels>
);
};

View File

@@ -0,0 +1,37 @@
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
// plane web components
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
export const PageNavigationPaneTabsList = () => {
// translation
const { t } = useTranslation();
return (
<Tab.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80 mx-3.5">
{({ selectedIndex }) => (
<>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
type="button"
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
>
{t(tab.i18n_label)}
</Tab>
))}
{/* active tab indicator */}
<div
className="absolute top-1/2 -translate-y-1/2 bg-custom-background-90 rounded transition-all duration-500 ease-in-out pointer-events-none"
style={{
left: `calc(${(selectedIndex / ORDERED_PAGE_NAVIGATION_TABS_LIST.length) * 100}% + 2px)`,
height: "calc(100% - 4px)",
width: `calc(${100 / ORDERED_PAGE_NAVIGATION_TABS_LIST.length}% - 4px)`,
}}
/>
</>
)}
</Tab.List>
);
};

View File

@@ -0,0 +1,21 @@
import type { ReactNode } from "react";
import type { EPageStoreType } from "@/plane-web/hooks/store";
import type { TPageInstance } from "@/store/pages/base-page";
export interface INavigationPaneExtensionProps<T = any> {
page: TPageInstance;
extensionData?: T;
storeType: EPageStoreType;
}
export interface INavigationPaneExtensionComponent<T = any> {
(props: INavigationPaneExtensionProps<T>): ReactNode;
}
export interface INavigationPaneExtension<T = any> {
id: string;
triggerParam: string;
component: INavigationPaneExtensionComponent<T>;
data?: T;
width?: number;
}

View File

@@ -0,0 +1,6 @@
// Export generic extension system interfaces
export type {
INavigationPaneExtensionProps,
INavigationPaneExtensionComponent,
INavigationPaneExtension,
} from "./extensions";